并发4️⃣管程⑤锁粒度、活跃性、ReentrantLock


1、锁的粒度

若对象的多个方法不会操作同一个变量。

细分锁的粒度,以提高并发性。

1.1、示例

示例:House 有 study 和 sleep 两个方法(互不影响)

  • House 类:执行方法时锁住当前对象。

    public class House {
        public void sleep() {
            synchronized (this) {
                LogUtils.debug("sleeping...");
                sleepSeconds(2);
                LogUtils.debug("OK");
            }
        }
    
        public void study() {
            synchronized (this) {
                LogUtils.debug("studying...");
                sleepSeconds(2);
                LogUtils.debug("OK");
            }
        }
    }
    
  • 测试

    • 开启两个线程,分别调用两个方法。

      House house = new House();
      
      new Thread(() -> house.sleep(), "t1").start();
      new Thread(() -> house.study(), "t2").start();
      
    • t1 执行 sleep() 时会导致 t2 阻塞。

      image-20220416220959821

1.2、优化

做法

  1. 定义两个成员变量,作为锁对象。

  2. 方法中的 synchronized(this) 改为对应的锁对象。

    public class House {
        private final Object sleepRoom = new Object();
        private final Object studyRoom = new Object();
    
        public void sleep() {
            synchronized (sleepRoom) {...}
        }
    
        public void study() {
            synchronized (studyRoom) {...}
        }
    }
    

测试

t1 和 t2 同时执行,互不影响。

image-20220416221450630

1.3、死锁问题

细分锁的粒度可能会导致死锁发生。

示例:对象 obj 有 m1() 和 m2() 两个方法,方法中 synchronized 相应的锁对象。

  • 某一时刻,线程 t1 正在执行 m1(),线程 t2 正在执行 m2()。
  • t1 要调用 m2(),且 t2 要调用 m1(),但锁都未被释放。
  • 两个线程都占用锁并等待对方释放锁,导致程序无法运行。

2、活跃性

2.1、死锁(?)

简单来说,死锁就是多个线程尝试获取对方正占有的资源

2.1.1、检测方式

  • 先 jps 获得进程 id,再 jstack 定位死锁。
  • jconsole 可视化工具。

2.1.2、示例:筷子问题

有 5 个人围成一桌,每人左右各有 1 支筷子。

  • 执行:需要同时获得左右筷子,才能吃饭。

  • 阻塞:若筷子被身边的人获得,需要等待释放。

  • 死锁:恰好每人拿到 1 根筷子,且都在等旁边的人释放。

    image-20220416223823819

Java 实现

  • Chopstick 类

    class Chopstick {
        private String name;
    
        public Chopstick() {
        }
    }
    
  • Person 类:继承 Thread 类

    • eat():打印输出语句,并睡眠 1 秒。

    • run():循环。先后对 left 和 right 加锁,执行 eat()。

      public class Person extends Thread {
          private Chopstick left, right;
      
          public Person(String name, Chopstick left, Chopstick right) {
              // 线程名
              setName(name);
              this.left = left;
              this.right = right;
          }
      
          @Override
          public void run() {
              while (true) {
                  synchronized (left) {
                      synchronized (right) { eat(); }
                  }
              }
          }
          private void eat() {
              LogUtils.debug("eating...");
              SleepUtils.sleepSeconds(1);
          }
      }
      

测试

  • 代码:5 人 5 筷子。

    Chopstick c1 = new Chopstick();
    Chopstick c2 = new Chopstick();
    Chopstick c3 = new Chopstick();
    Chopstick c4 = new Chopstick();
    Chopstick c5 = new Chopstick();
    
    new Person("老大", c1, c2).start();
    new Person("二货", c2, c3).start();
    new Person("张三", c3, c4).start();
    new Person("李四", c4, c5).start();
    new Person("王五", c5, c1).start();
    
  • 结果:程序运行一段时间后不再执行,即死锁。

    image-20220416230004066

2.2、饥饿

  • OS:某个进程一直得不到资源。
  • Java:优先级低的线程一直无法获得 CPU 时间片,但没有结束(仍处于就绪状态)。

2.3、说明

活跃性问题(死锁、活锁、饥饿),都可用 ReentrantLock 来解决。

3、ReentrantLock(?)

ReentrantLock:可重入锁

3.1、说明

与 的使用基本相同,可对比理解。

3.1.1、对比

ReentrantLock 支持中断、超时时间、公平锁、多个条件变量。

synchronized ReentrantLock
含义 对象锁(重量级) 可重入锁
可重入 ? ?
可中断 进入 EntryList 的线程会一直阻塞并尝试获取锁 lockInteruptibly()
可被其它线程中断阻塞状态
超时时间 进入 EntryList 的线程会无限期尝试获取锁,直到成功竞争锁 trywait()
可设置超时时间,超过指定时间后不再尝试获取锁
公平锁 不支持,Owner 唤醒 EntryList 的所有阻塞线程进行竞争 支持,先进入 EntryList 的先成为新的 Owner(FIFO)
条件变量 单个,即 WaitSet await()
支持多个,用于表示不同的等待条件

3.1.2、语法

  • synchronized

    • 步骤:创建锁对象、加锁。

    • 在字节码层面自动加锁和解锁,monitorenter 和 monitorexit。

      Object lock = new Object();
      
      synchronized(lock){
          // 临界区
      }
      
  • ReentrantLock

    • 步骤:创建锁对象、加锁、释放锁。

    • 需要手动调用加锁和解锁方法,lock() 和 unlock()。

    • 需结合 try 代码块,unlock() 放在 finally 块首行。

      ReentrantLock reentrantLock = new ReentrantLock();
      
      reentrantLock.lock();
      try {
          // 临界区
      } finally {
          reentrantLock.unlock();
      }
      

3.2、特点

3.2.1、可重入

  • 不可重入:线程 t1 获得对象 obj 的锁,在释放锁之前无法再次获取锁。
  • 可重入:t1 在释放 obj 锁之前可再次获取锁。

测试

对 reentrantLock 对象加锁,且调用另一个会对 reentrantLock 加锁的方法。

  • 代码

    ReentrantLock reentrantLock = new ReentrantLock();
    
    public void reentry() {
        reentrantLock.lock();
        try {
            LogUtils.debug("调用 reentry()");
            m1();
        } finally { reentrantLock.unlock(); }
    }
    
    private void m1() {
        reentrantLock.lock();
        try {
            LogUtils.debug("调用 m1()");
            m2();
        } finally { reentrantLock.unlock(); }
    }
    
    private void m2() {
        reentrantLock.lock();
        try {
            LogUtils.debug("调用 m2()");
        } finally { reentrantLock.unlock(); }
    }
    
  • 测试:成功进入 3 个方法。

    image-20220417150857926

3.2.2、可中断

lockInterruptibly()

  • lockInterruptibly() 方法可被中断,因此会抛出 InterruptedException 异常。

  • 其余步骤与 lock() 相同。

    try {
        // 加锁
        reentrantLock.lockInterruptibly();
        try {
            // 临界区
            LogUtils.debug("得到锁");
        } finally {
            // 释放锁
            reentrantLock.unlock();
            LogUtils.debug("释放锁");
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
        LogUtils.debug("尝试获取锁时被中断");
    }
    

3.2.3、超时时间

尝试获取锁时,超过指定时间则不再尝试获取。

返回值为 boolean

  • tryLock():不等待。

  • tryLock(long, TimeUnit):指定时间单位,等待 long 单位时间。

    if (!reentrantLock.tryLock()) {
        LogUtils.debug("超时,不再尝试获取");
    }
    try {
        LogUtils.debug("得到锁");
    } finally {
        reentrantLock.unlock();
    }
    

解决 2.1.2 死锁问题

  • Chopstick 继承 ReentrantLock 类

  • 将 synchronized 改成 ReentrantLock 的相关方法。

    1. 尝试获取 left,失败(返回 false)则进入下一轮循环。

    2. 已获取 left,尝试获取 right

      • 成功:拿到两幅侉子,调用 eat() 吃饭。

      • 失败:执行外层的 finally,释放 left(相当于放下左筷子)。

        public class Person extends Thread {
            // 省略其它
        
            @Override
            public void run() {
                while (true) {
                    // 尝试获取左筷子
                    if (left.tryLock()) {
                        try {
                            // 尝试获取右筷子
                            if (right.tryLock()) {
                                try { eat(); }
                                finally { right.unlock(); }
                            }
                        } finally {
                            left.unlock();
                        }
                    }
                }
            }
        }
        
        class Chopstick extends ReentrantLock {}
        
  • 测试:不会死锁,也不会出现饥饿问题。

    image-20220417160914352

3.2.4、公平锁

ReentrantLock 默认是非公平锁。

在构造方法的参数列表,可设置 true 表示开启公平锁。

  • 空参:nonFair

  • boolean:true 表示开启

    public ReentrantLock() {
        sync = new NonfairSync();
    }
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    

说明

  • 公平锁可解决饥饿问题
  • 公平锁会降低并发性,使用上不如 trylock()

3.2.5、条件变量

注:使用方式相同,细节不同

Object ReentrantLock
使用前提 获得对象锁(synchronized) 获取条件变量(conditionObject)
条件变量个数 一个,即 WaitSet 多个(细分不同条件,)
等待 wait() await()
唤醒 notify() / notifyAll() signal() / signalAll()