3.15 ReentrantLock
相对于 synchronized
它具备如下特点
可中断(使用其他线程或者方法取消锁)
可以设置超时时间(一段时间内未获取到锁,放弃去争抢锁,执行一些其他逻辑操作)
可以设置为公平锁(先进先出,防止出现锁饥饿现象)
支持多个条件变量(允许有多个WaitSet,不满足条件1时,去waitSet1中等待,不满足2时,去waitSet2中等待。当然唤醒时,也可以根据条件进行唤醒)
这里的中断是指,别的线程可以破坏你的blocking状态,而不是指自己中断阻塞状态
**相同点:**与 synchronized 一样,都支持可重入(同一线程对对象可以重复加锁)
基本语法
// 获取锁 reentrantLock.lock(); try { // 临界区 } finally { // 释放锁 reentrantLock.unlock(); }
3.15.1 可重入
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁。
如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
@Slf4j(topic = "c.Test17") public class Test17 { static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { method1(); } public static void method1() { lock.lock(); try { log.debug("execute method1"); method2(); } finally { lock.unlock(); } } public static void method2() { lock.lock(); try { log.debug("execute method2"); method3(); } finally { lock.unlock(); } } public static void method3() { lock.lock(); try { log.debug("execute method3"); } finally { lock.unlock(); } } }
输出:
15:33:20.791 c.Test17 [main] - execute method1 15:33:20.794 c.Test17 [main] - execute method2 15:33:20.794 c.Test17 [main] - execute method3
3.15.2 可打断
/** * @author lxy * @version 1.0 * @Description ReentrantLock可打断特性示例 * @date 2022/7/3 17:29 * 使用lock.lockInterruptibly()的优点:可以防止死锁的产生,避免长时间的等待。 */ @Slf4j(topic = "c.Test18") public class Test18 { private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { Thread t1 = new Thread(() -> { try { //如果没有竞争那么此方法就会获取lock对象锁 //如果有竞争就进入阻塞队列,可以被其他线程用 interrupt 方法 log.debug("尝试获取锁"); lock.lockInterruptibly(); } catch (InterruptedException e) { e.printStackTrace(); log.debug("没有获得锁,返回"); return; } try { log.debug("获取到锁"); } finally {//为啥俩try不能被合并:因为被打断的话,不能unlock,只有获取锁了才能继续往下走,往下走又必须来一个finally来保证锁一定被释放 lock.unlock();//释放锁 } }, "t1"); //1.当只有一个t1线程时 //t1.start(); //2.当有t1和main线程时 // lock.lock(); // t1.start(); //3.当t1被interrupt打断时 // lock.lock(); // t1.start(); // Sleeper.sleep(2); // log.debug("打断t1"); // t1.interrupt(); } }
输出:
- 当只有一个t1线程时
17:55:18.341 c.Test18 [t1] - 尝试获取锁 17:55:18.343 c.Test18 [t1] - 获取到锁
当有t1和main线程时
当t1被interrupt打断时
如果将上锁代码替换成lock.lock();
==可打断锁的意义:==可以防止死锁的产生,避免长时间的等待。
3.15.3 锁超时介绍以及应用
可打断和锁超时的区别:可打断是线程t1调用interrupt方法进行打断阻塞状态,是被动的;而锁超时是超过一定时间就放弃获得,是主动的。
使用方式一:tryLock
@Slf4j(topic = "c.Test19") public class Test19 { private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { Thread t1 = new Thread(() -> { log.debug("尝试获得锁"); if (!lock.tryLock()) { log.debug("获取不到锁"); return; } try { log.debug("获得到锁"); } finally { lock.unlock(); } }, "t1"); lock.lock(); log.debug("获得到锁"); t1.start(); } }
输出:
19:27:15.757 c.Test19 [main] - 获得到锁 19:27:15.759 c.Test19 [t1] - 尝试获得锁 19:27:15.759 c.Test19 [t1] - 获取不到锁
- 使用方式2:使用
trylock(long timeout,TimeUnit unit)
且超时
输出:超时后放弃获取锁
- 使用方式三:使用
trylock(long timeout,TimeUnit unit)
且未超时
@Slf4j(topic = "c.Test19") public class Test19 { private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { Thread t1 = new Thread(() -> { log.debug("尝试获得锁"); try { if(!lock.tryLock(2, TimeUnit.SECONDS)) { log.debug("获取不到锁"); return; } }catch (InterruptedException e){ e.printStackTrace(); } try { log.debug("获得到锁"); } finally { lock.unlock(); } }, "t1"); lock.lock(); log.debug("获得到锁"); t1.start(); Sleeper.sleep(1); lock.unlock(); log.debug("释放了锁"); } }
输出:
锁超时的应用-解决哲学家就餐问题
//测试死锁 public class TestDeadLock { public static void main(String[] args) { Chopstick c1 = new Chopstick("1"); Chopstick c2 = new Chopstick("2"); Chopstick c3 = new Chopstick("3"); Chopstick c4 = new Chopstick("4"); Chopstick c5 = new Chopstick("5"); new Philosopher("苏格拉底", c1, c2).start(); new Philosopher("柏拉图", c2, c3).start(); new Philosopher("亚里士多德", c3, c4).start(); new Philosopher("赫拉克利特", c4, c5).start(); new Philosopher("阿基米德", c5, c1).start(); } } //哲学家类 @Slf4j(topic = "c.Philosopher") class Philosopher extends Thread { Chopstick left; Chopstick right; public Philosopher(String name, Chopstick left, Chopstick right) { super(name);//线程名称 this.left = left; this.right = right; } @Override public void run() { while (true) { // 尝试获得左手筷子 if(left.tryLock()){ try { // 尝试获得右手筷子 if(right.tryLock()){ try { eat(); }finally {//为了保证获取右手锁后可以成功释放,所以需要加一个 try...catch块 right.unlock(); } } }finally {//为了保证获取左手锁后可以成功释放,所以需要加一个try...catch left.unlock(); } } } } Random random = new Random(); private void eat() { log.debug("eating..."); Sleeper.sleep(0.5); } } //筷子类 class Chopstick extends ReentrantLock {//让筷子锁对象有重入锁的一些特征 String name; public Chopstick(String name) { this.name = name; } @Override public String toString() { return "筷子{" + name + '}'; } }
输出:
14:20:25.184 c.Philosopher [亚里士多德] - eating... 14:20:25.199 c.Philosopher [苏格拉底] - eating... 14:20:25.705 c.Philosopher [柏拉图] - eating... 14:20:25.705 c.Philosopher [赫拉克利特] - eating... 14:20:26.205 c.Philosopher [苏格拉底] - eating...//哲学家们可以正常的进餐
3.15.4 公平锁
ReentrantLock 默认是不公平的,但是可以通过构造方法来设置是否是公平锁。
公平的举例:
不公平的锁,比如synchronized;每次当有线程A占用锁对象,其他线程会进入阻塞队列进行等待,当A使用完后,释放锁,其他线程就会进行竞争锁,抢到了就可以使用。所以这个过程时不公平的。
公平的锁,比如 ReentrantLock ,按照线程进入阻塞队列的顺序来获得锁,先来先得。
公平锁主要来解决线程饥饿问题。前面我们使用tryLock()也可以进行解决。所以公平锁一般没有必要,会降低并发度,后面分析原理时会讲解
**注:**关于公平锁的示例代码,这里不再演示。后面源码剖析时详细介绍。
3.15.5 条件变量
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的(休息室),这就好比
synchronized 是那些不满足条件的线程都在一间休息室等消息
而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
使用要点:
await 前需要获得锁(同synchronized)
await 执行后,会释放锁,进入 conditionObject 等待
await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
竞争 lock 锁成功后,从 await 后继续执行
演示代码
@Slf4j(topic = "c.Test20") public class Test20 { static boolean hasCigarette = false; static boolean hasTakeout = false; static ReentrantLock ROOM = new ReentrantLock(); //等待烟的休息室 static Condition waitCigaretteSet = ROOM.newCondition(); static Condition waitTakeoutSet = ROOM.newCondition(); public static void main(String[] args) { new Thread(()->{ ROOM.lock(); try { log.debug("烟送到没?[{}]",hasCigarette); while (!hasCigarette){ log.debug("没烟,先歇会!"); try { waitCigaretteSet.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("可以开始干活了"); }finally { ROOM.unlock(); } },"小南").start(); new Thread(()->{ ROOM.lock(); try { log.debug("外卖送到没?[{}]",hasTakeout); while (!hasTakeout){ log.debug("没外卖,先歇会!"); try { waitTakeoutSet.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("可以开始干活了"); }finally { ROOM.unlock(); } },"小女").start(); Sleeper.sleep(1); new Thread(()->{ ROOM.lock(); try { hasTakeout = true; log.debug("外卖送到了"); waitTakeoutSet.signal(); }finally { ROOM.unlock(); } },"送外卖的").start(); Sleeper.sleep(1); new Thread(()->{ ROOM.lock(); try { hasCigarette = true; log.debug("烟送到了!"); waitCigaretteSet.signal(); }finally { ROOM.unlock(); } },"送烟的").start(); } }
输出:
3.15.5 同步模式之顺序控制
本章小结
本章我们需要重点掌握的是
分析多线程访问共享资源时,哪些代码片段属于临界区
使用 synchronized 互斥解决临界区的线程安全问题
掌握 synchronized 锁对象语法
掌握 synchronzied 加载成员方法和静态方法语法
掌握 wait/notify 同步方法
使用 lock 互斥解决临界区的线程安全问题
掌握 lock 的使用细节:可打断、锁超时、公平锁、条件变量
学会分析变量的线程安全性、掌握常见线程安全类的使用
了解线程活跃性问题:死锁、活锁、饥饿
应用方面
互斥:使用 synchronized 或 Lock 达到共享资源互斥效果
同步:使用 wait/notify 或 Lock 的条件变量来达到线程间通信效果
原理方面
monitor(管程)、synchronized 、wait/notify 原理
synchronized 进阶原理
park & unpark 原理
模式方面
同步模式之保护性暂停
异步模式之生产者消费者
同步模式之顺序控制
注意:
synchronized的互斥是为了临界区的代码不上下文切换,产生指令交错,从而保证临界区代码的原子性。synchronized的同步是为了解决当条件不满足时线程等待,条件满足时继续运行。ReentrantLock也可以实现互斥和同步。
monitor的源码是用C++写的,Java也实现了Monitor锁,即ReentrantLock