并发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、活跃性

OS:活跃性

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、说明

二者使用基本相同,可对比理解。

  • synchronized:底层是 C++ 实现的 Monitor。
  • ReentrantLock:Java 实现。

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()
posted @ 2022-04-17 14:51  Jaywee  阅读(91)  评论(0编辑  收藏  举报

👇