Java并发编程(五) (共享模型之管程)_ 死锁、哲学家就餐问题、ReentrantLock (锁重入、锁超时、可中断、公平锁、条件变量)、同步模式之顺序控制·
一、多把锁· 小故事 一间大屋子有两个功能:睡觉、学习,互不相干。 现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低小南获得锁之后, 学完习之后, 小女才能进来睡觉。 解决方法是准备多个房间(多个对象锁) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Slf4j(topic = "guizy.BigRoomTest") public class BigRoomTest { public static void main (String[] args) { BigRoom bigRoom = new BigRoom (); new Thread (() -> bigRoom.sleep(), "小南" ).start(); new Thread (() -> bigRoom.study(), "小女" ).start(); } } @Slf4j(topic = "guizy.BigRoom") class BigRoom { public void sleep () { synchronized (this ) { log.debug("sleeping 2 小时" ); Sleeper.sleep(2 ); } } public void study () { synchronized (this ) { log.debug("study 1 小时" ); Sleeper.sleep(1 ); } } }
// 相当于串行执行, 因为锁对象是整个屋子, 所以并发性很低 12:16:15.952 guizy.BigRoom [小南] - sleeping 2 小时 12:16:17.954 guizy.BigRoom [小女] - study 1 小时
1 2 3 4 5 6 7 8 9 10 11 12 @Slf4j(topic = "guizy.BigRoomTest") public class BigRoomTest { private static final BigRoom sleepRoom = new BigRoom (); private static final BigRoom studyRoom = new BigRoom (); public static void main (String[] args) { new Thread (() -> sleepRoom.sleep(), "小南" ).start(); new Thread (() -> studyRoom.study(), "小女" ).start(); } }
// 因为使用的是不同的锁对象 12:18:50.580 guizy.BigRoom [小女] - study 1 小时 12:18:50.580 guizy.BigRoom [小南] - sleeping 2 小时
将锁的粒度细分 好处,是可以增强并发度 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁 二、 活跃性· 因为某种原因,使得代码一直无法执行完毕,这样的现象叫做 活跃性 活跃性相关的一系列问题都可以用 ReentrantLock 进行解决。 1、死锁 (重点)· 有这样的情况:一个线程需要 同时获取多把锁 ,这时就容易发生死锁 如:线程1获取A对象锁, 线程2获取B对象锁; 此时线程1又想获取B对象锁, 线程2又想获取A对象锁; 它们都等着对象释放锁, 此时就称为死锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public static void main (String[] args) { final Object A = new Object (); final Object B = new Object (); new Thread (()->{ synchronized (A) { try { Thread.sleep(2000 ); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (B) { } } }).start(); new Thread (()->{ synchronized (B) { try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (A) { } } }).start(); }
1.2、发生死锁的必要条件 (重点)· 互斥条件 请求和保持条件 进程已经拥有了至少一种资源,同时又去申请其他资源。因为其他资源被别的进程所使用,该进程进入阻塞状态,并且不释放自己已有的资源 不可抢占条件 进程对已获得的资源在未使用完成前不能被强占,只能在进程使用完后自己释放 循环等待条件 1.3、定位死锁的方法· 方式一、JPS + JStack 进程ID
在Java控制台中的Terminal中输入 jps 指令可以查看正在运行中的进程ID,使用 jstack 进程ID 可以查看进程状态。 方式二、 jconsole检测死锁
1.4、死锁举例 - 哲学家就餐问题 (重点)· 有五位哲学家,围坐在圆桌旁。
他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。 如果筷子被身边的人拿着,自己就得等待 当每个哲学家即线程持有一根筷子时,他们都在等待另一个线程释放锁,因此造成了死锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 @Slf4j(topic = "guizy.PhilosopherEat") public class PhilosopherEat { 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 = "guizy.Philosopher") class Philosopher extends Thread { final Chopstick left; final Chopstick right; public Philosopher (String name, Chopstick left, Chopstick right) { super (name); this .left = left; this .right = right; } @Override public void run () { while (true ) { synchronized (left) { synchronized (right) { eat(); } } } } private void eat () { log.debug("eating..." ); Sleeper.sleep(0.5 ); } } class Chopstick { String name; public Chopstick (String name) { this .name = name; } @Override public String toString () { return "筷子{" + name + '}' ; } }
// 程序只执行了下面的打印, 没有停止, 没有打印 15:04:55.346 guizy.Philosopher [苏格拉底] - eating… 15:04:55.346 guizy.Philosopher [亚里士多德] - eating… 15:04:55.850 guizy.Philosopher [亚里士多德] - eating… 15:04:55.850 guizy.Philosopher [苏格拉底] - eating… 15:04:56.351 guizy.Philosopher [亚里士多德] - eating… 15:04:56.852 guizy.Philosopher [亚里士多德] - eating…
通过jps, jstack 进程id查看死锁原因 Found one Java-level deadlock: 发现了一个Java级别的死锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 Found one Java-level deadlock: ============================= "阿基米德" : waiting to lock monitor 0x000000001ae2a358 (object 0x00000000d6ea7420 , a com.guizy.reentrantlock.Chopstick), which is held by "苏格拉底" "苏格拉底" : waiting to lock monitor 0x0000000017fb3518 (object 0x00000000d6ea7430 , a com.guizy.reentrantlock.Chopstick), which is held by "柏拉图" "柏???图" : waiting to lock monitor 0x0000000017fb3468 (object 0x00000000d6ea7440 , a com.guizy.reentrantlock.Chopstick), which is held by "亚里士多德" "亚里士多德" : waiting to lock monitor 0x0000000017fb0bd8 (object 0x00000000d6ea7450 , a com.guizy.reentrantlock.Chopstick), which is held by "赫拉克利特" "赫拉克利特" : waiting to lock monitor 0x0000000017fb0c88 (object 0x00000000d6ea7460 , a com.guizy.reentrantlock.Chopstick), which is held by "阿基米德" Java stack information for the threads listed above: =================================================== "阿基米德" : at com.guizy.reentrantlock.Philosopher.run(PhilosopherEat.java:47 ) - waiting to lock <0x00000000d6ea7420 > (a com.guizy.reentrantlock.Chopstick) - locked <0x00000000d6ea7460 > (a com.guizy.reentrantlock.Chopstick) "苏格拉底" : at com.guizy.reentrantlock.Philosopher.run(PhilosopherEat.java:47 ) - waiting to lock <0x00000000d6ea7430 > (a com.guizy.reentrantlock.Chopstick) - locked <0x00000000d6ea7420 > (a com.guizy.reentrantlock.Chopstick) "柏拉图" : at com.guizy.reentrantlock.Philosopher.run(PhilosopherEat.java:47 ) - waiting to lock <0x00000000d6ea7440 > (a com.guizy.reentrantlock.Chopstick) - locked <0x00000000d6ea7430 > (a com.guizy.reentrantlock.Chopstick) "亚里士多德" : at com.guizy.reentrantlock.Philosopher.run(PhilosopherEat.java:47 ) - waiting to lock <0x00000000d6ea7450 > (a com.guizy.reentrantlock.Chopstick) - locked <0x00000000d6ea7440 > (a com.guizy.reentrantlock.Chopstick) "赫拉克利特" : at com.guizy.reentrantlock.Philosopher.run(PhilosopherEat.java:47 ) - waiting to lock <0x00000000d6ea7460 > (a com.guizy.reentrantlock.Chopstick) - locked <0x00000000d6ea7450 > (a com.guizy.reentrantlock.Chopstick) Found 1 deadlock.
1.5、避免死锁的方法· 在线程使用锁对象时, 采用固定加锁的顺序 , 可以使用Hash值的大小来确定加锁的先后 尽可能缩减加锁的范围, 等到操作共享变量的时候才加锁 使用可释放的定时锁 (一段时间申请不到锁的权限了, 直接释放掉) 顺序加锁的解决方案
2、活锁· 活锁出现在两个线程 互相改变对方的结束条件 ,谁也无法结束。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Slf4j(topic = "create.thread") public class CreateThread { static final Object lock = new Object (); static volatile int count = 10 ; public static void main (String[] args) { new Thread (() -> { while (count > 0 ) { try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } count--; log.debug("count:{}" , count); } }, "t1" ).start(); new Thread (() -> { while (count < 20 ) { try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } count++; log.debug("count:{}" , count); } }, "t2" ).start(); } }
2.1、避免活锁的方法· 在线程执行时,中途给予 不同的间隔时间 , 让某个线程先结束即可。 2.2、死锁与活锁的区别· 死锁是因为线程互相持有对象想要的锁,并且都不释放,最后到时线程阻塞 ,停止运行 的现象。 活锁是因为线程间修改了对方的结束条件,而导致代码一直在运行 ,却一直运行不完 的现象。 3、饥饿· 某些线程因为优先级太低,导致一直无法获得资源的现象。 在使用顺序加锁时,可能会出现饥饿现象 三、 ReentrantLock (重点)·
ReentrantLock 的特点 (synchronized不具备的)
支持锁重入 可重入锁是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此 有权利再次获取这把锁 可中断 lock.lockInterruptibly() : 可以被其他线程打断的中断锁 可以设置超时时间 lock.tryLock(时间) : 尝试获取锁对象, 如果超过了设置的时间, 还没有获取到锁, 此时就退出阻塞队列, 并释放掉自己拥有的锁 可以设置为公平锁 (先到先得) 默认是非公平, true为公平 new ReentrantLock(true) 支持多个条件变量( 有多个waitset) (可避免虚假唤醒) - lock.newCondition()创建条件变量对象; 通过条件变量对象调用 await/signal方法, 等待/唤醒 基本语法
1 2 3 4 5 6 7 8 9 10 11 private ReentrantLock lock = new ReentrantLock ();lock.lock(); try { }finally { lock.unlock(); }
ReentrantLock特点· 1、支持锁重入· 可重入锁是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此 有权利再次获取这把锁 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Slf4j(topic = "guizy.ReentrantTest") public class ReentrantTest { private static ReentrantLock lock = new ReentrantLock (); public static void main (String[] args) { lock.lock(); try { log.debug("entry main..." ); m1(); } finally { lock.unlock(); } } private static void m1 () { lock.lock(); try { log.debug("entry m1..." ); m2(); } finally { lock.unlock(); } } private static void m2 () { log.debug("entry m2...." ); } }
13:54:29.324 guizy.ReentrantTest [main] - entry main… 13:54:29.326 guizy.ReentrantTest [main] - entry m1… 13:54:29.326 guizy.ReentrantTest [main] - entry m2…
2、可中断 (针对于lockInterruptibly()方法获得的中断锁) 直接退出阻塞队列, 获取锁失败· synchronized 和 reentrantlock.lock() 的锁, 是不可被打断的; 也就是说别的线程已经获得了锁, 我的线程就需要一直等待下去. 不能中断
可被中断的锁, 通过lock.lockInterruptibly()获取的锁对象, 可以通过调用阻塞线程的interrupt()方法 如果某个线程处于阻塞状态,可以调用其interrupt方法让其停止阻塞,**获得锁失败 **处于阻塞状态的线程,被打断了就不用阻塞了,直接停止运行 可中断的锁, 在一定程度上可以被动的减少死锁的概率, 之所以被动, 是因为我们需要手动调用阻塞线程的interrupt方法; 测试使用lock.lockInterruptibly()可以从阻塞队列中,打断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 @Slf4j(topic = "guizy.ReentrantTest") public class ReentrantTest { private static final ReentrantLock lock = new ReentrantLock (); public static void main (String[] args) { Thread t1 = new Thread (() -> { log.debug("t1线程启动..." ); try { lock.lockInterruptibly(); } catch (InterruptedException e) { e.printStackTrace(); log.debug("等锁的过程中被打断" ); return ; } try { log.debug("t1线程获得了锁" ); } finally { lock.unlock(); } }, "t1" ); lock.lock(); log.debug("main线程获得了锁" ); t1.start(); try { Sleeper.sleep(1 ); t1.interrupt(); log.debug("执行打断" ); } finally { lock.unlock(); } } }
1 2 3 4 5 6 7 8 9 10 11 14 :18 :09.145 guizy.ReentrantTest [main] - main线程获得了锁14 :18 :09.148 guizy.ReentrantTest [t1] - t1线程启动...14 :18 :10.149 guizy.ReentrantTest [main] - 执行打断14 :18 :10.149 guizy.ReentrantTest [t1] - 等锁的过程中被打断java.lang.InterruptedException at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898 ) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222 ) at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335 ) at com.guizy.reentrantlock.ReentrantTest.lambda$main$0 (ReentrantTest.java:25 ) at java.lang.Thread.run(Thread.java:748 )
测试使用lock.lock()不可以从阻塞队列中打断, 一直等待别的线程释放锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Slf4j(topic = "guizy.ReentrantTest") public class ReentrantTest { private static final ReentrantLock lock = new ReentrantLock (); public static void main (String[] args) { Thread t1 = new Thread (() -> { log.debug("t1线程启动..." ); lock.lock(); try { log.debug("t1线程获得了锁" ); } finally { lock.unlock(); } }, "t1" ); lock.lock(); log.debug("main线程获得了锁" ); t1.start(); try { t1.interrupt(); Sleeper.sleep(4 ); log.debug("main线程执行打断" ); } finally { lock.unlock(); } } }
14:21:01.329 guizy.ReentrantTest [main] - main线程获得了锁 14:21:01.331 guizy.ReentrantTest [t1] - t1线程启动… 14:21:01.333 guizy.ReentrantTest [main] - main线程执行打断 14:21:05.333 guizy.ReentrantTest [t1] - t1线程获得了锁
lock()锁不能被打断 , 在主线程中调用t1.interrupt(), 没用, 当主线程释放锁之后, t1获得了锁3、锁超时 (lock.tryLock()) 直接退出阻塞队列, 获取锁失败· 防止无限制等待, 减少死锁
使用 lock.tryLock() 方法会返回获取锁是否成功。如果成功则返回true,反之则返回false。 并且tryLock方法可以设置指定等待时间 ,参数为:tryLock(long timeout, TimeUnit unit) , 其中timeout为最长等待时间,TimeUnit为时间单位 获取锁的过程中, 如果超过等待时间, 或者被打断, 就直接从阻塞队列移除, 此时获取锁就失败了, 不会一直阻塞着 ! (可以用来实现死锁问题)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @Slf4j(topic = "guizy.ReentrantTest") public class ReentrantTest { private static final 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(); Sleeper.sleep(2 ); log.debug("释放了锁" ); lock.unlock(); } }
14:52:19.726 guizy.WaitNotifyTest [main] - 获得到锁 14:52:19.728 guizy.WaitNotifyTest [t1] - 尝试获得锁 14:52:19.728 guizy.WaitNotifyTest [t1] - 获取立刻失败,返回 14:52:21.728 guizy.WaitNotifyTest [main] - 释放了锁
设置等待时间, 超过等待时间还没有获得锁, 失败, 从阻塞队列移除该线程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @Slf4j(topic = "guizy.ReentrantTest") public class ReentrantTest { private static final ReentrantLock lock = new ReentrantLock (); public static void main (String[] args) { Thread t1 = new Thread (() -> { log.debug("尝试获得锁" ); try { if (!lock.tryLock(1 , TimeUnit.SECONDS)) { log.debug("获取锁超时,返回" ); return ; } } catch (InterruptedException e) { log.debug("被打断了, 获取锁失败, 返回" ); e.printStackTrace(); return ; } try { log.debug("获得到锁" ); } finally { lock.unlock(); } }, "t1" ); lock.lock(); log.debug("获得到锁" ); t1.start(); Sleeper.sleep(2 ); log.debug("main线程释放了锁" ); lock.unlock(); } }
// 超时的打印 14:55:56.647 guizy.WaitNotifyTest [main] - 获得到锁 14:55:56.651 guizy.WaitNotifyTest [t1] - 尝试获得锁 14:55:57.652 guizy.WaitNotifyTest [t1] - 获取锁超时,返回 14:55:58.652 guizy.WaitNotifyTest [main] - main线程释放了锁
// 中断的打印 14:56:41.258 guizy.WaitNotifyTest [main] - 获得到锁 14:56:41.260 guizy.WaitNotifyTest [main] - main线程释放了锁 14:56:41.261 guizy.WaitNotifyTest [t1] - 尝试获得锁 14:56:41.261 guizy.WaitNotifyTest [t1] - 被打断了, 获取锁失败, 返回 java.lang.InterruptedException
通过lock.tryLock()来解决, 哲学家就餐问题 (重点)· lock.tryLock(时间) : 尝试获取锁对象, 如果超过了设置的时间, 还没有获取到锁, 此时就退出阻塞队列, 并释放掉自己拥有的锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 @Slf4j(topic = "guizy.PhilosopherEat") public class PhilosopherEat { 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 = "guizy.Philosopher") class Philosopher extends Thread { final Chopstick left; final 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 { right.unlock(); } } } finally { left.unlock(); } } } } 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 + '}' ; } }
15:16:01.793 guizy.Philosopher [亚里士多德] - eating… 15:16:01.795 guizy.Philosopher [苏格拉底] - eating… 15:16:02.293 guizy.Philosopher [亚里士多德] - eating… 15:16:02.295 guizy.Philosopher [苏格拉底] - eating… 15:16:02.794 guizy.Philosopher [赫拉克利特] - eating… 15:16:02.796 guizy.Philosopher [苏格拉底] - eating… 15:16:03.294 guizy.Philosopher [赫拉克利特] - eating… 15:16:03.296 guizy.Philosopher [柏拉图] - eating… 15:16:03.795 guizy.Philosopher [赫拉克利特] - eating… 15:16:03.797 guizy.Philosopher [苏格拉底] - eating… 15:16:04.295 guizy.Philosopher [亚里士多德] - eating… 15:16:04.297 guizy.Philosopher [苏格拉底] - eating… 15:16:04.796 guizy.Philosopher [亚里士多德] - eating… 15:16:04.798 guizy.Philosopher [阿基米德] - eating… 15:16:05.296 guizy.Philosopher [柏拉图] - eating… 15:16:05.299 guizy.Philosopher [赫拉克利特] - eating…
4、公平锁 new ReentrantLock(true)· ReentrantLock默认是非公平锁, 可以指定为公平锁。 在线程获取锁失败,进入阻塞队列时,先进入 的会在锁被释放后先获得 锁。这样的获取方式就是公平 的。一般不设置ReentrantLock为公平的, 会降低并发度 Synchronized底层的Monitor锁就是不公平的, 和谁先进入阻塞队列是没有关系的。 1 2 3 ReentrantLock lock = new ReentrantLock (true );
4.1、什么是公平锁? 什么是非公平锁?· 公平锁 (new ReentrantLock(true))· 公平锁, 可以把竞争的线程放在一个先进先出的阻塞队列上 只要持有锁的线程执行完了, 唤醒阻塞队列中的下一个线程获取锁即可; 此时先进入阻塞队列的线程先获取到锁 非公平锁 (synchronized, new ReentrantLock())· 非公平锁, 当阻塞队列中已经有等待的线程A了, 此时后到的线程B, 先去尝试看能否获得到锁对象. 如果获取成功, 此时就不需要进入阻塞队列了. 这样以来后来的线程B就先活的到锁了 所以公平和非公平的区别 : 线程执行同步代码块时, 是否回去尝试获取锁 , 如果会尝试获取锁, 那就是非公平的, 如果不会尝试获取锁, 直接进入阻塞队列, 再等待被唤醒, 那就是公平的
如果不进如队列呢? 线程一直尝试获取锁不就行了?一直尝试获取锁, 在synchronized轻量级锁升级为重量级锁时, 做的一个优化, 叫做自旋锁, 一般很消耗资源, cpu一直空转, 最后获取锁也失败, 所以不推荐使用。在jdk6对于自旋锁有一个机制, 在重试获得锁指定次数就失败等等 5、条件变量 (可避免虚假唤醒) - lock.newCondition()创建条件变量对象; 通过条件变量对象调用await/signal方法, 等待/唤醒· Synchronized 中也有条件变量,就是Monitor监视器中的 waitSet等待集合,当条件不满足时进入waitSet 等待ReentrantLock 的条件变量比 synchronized 强大之处在于,它是 支持多个条件变量。 这就好比synchronized 是那些不满足条件的线程都在一间休息室等通知; (此时会造成虚假唤醒) , 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒; (可以避免虚假唤醒) 使用要点:
await 前需要 获得锁 await 执行后,会释放锁,进入 conditionObject (条件变量)中等待 await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁竞争 lock 锁成功后,从 await 后继续执行 signal 方法用来唤醒条件变量(等待室)汇总的某一个等待的线程 signalAll方法, 唤醒条件变量(休息室)中的所有线程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 @Slf4j(topic = "guizy.ConditionVariable") public class ConditionVariable { private static boolean hasCigarette = false ; private static boolean hasTakeout = false ; private static final ReentrantLock lock = new ReentrantLock (); static Condition waitCigaretteSet = lock.newCondition(); static Condition waitTakeoutSet = lock.newCondition(); public static void main (String[] args) { new Thread (() -> { lock.lock(); try { log.debug("有烟没?[{}]" , hasCigarette); while (!hasCigarette) { log.debug("没烟,先歇会!" ); try { waitCigaretteSet.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("烟来咯, 可以开始干活了" ); } finally { lock.unlock(); } }, "小南" ).start(); new Thread (() -> { lock.lock(); try { log.debug("外卖送到没?[{}]" , hasTakeout); while (!hasTakeout) { log.debug("没外卖,先歇会!" ); try { waitTakeoutSet.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("外卖来咯, 可以开始干活了" ); } finally { lock.unlock(); } }, "小女" ).start(); Sleeper.sleep(1 ); new Thread (() -> { lock.lock(); try { log.debug("送外卖的来咯~" ); hasTakeout = true ; waitTakeoutSet.signal(); } finally { lock.unlock(); } }, "送外卖的" ).start(); Sleeper.sleep(1 ); new Thread (() -> { lock.lock(); try { log.debug("送烟的来咯~" ); hasCigarette = true ; waitCigaretteSet.signal(); } finally { lock.unlock(); } }, "送烟的" ).start(); } }
15:08:58.231 guizy.WaitNotifyTest [小南] - 有烟没?[false] 15:08:58.234 guizy.WaitNotifyTest [小南] - 没烟,先歇会! 15:08:58.235 guizy.WaitNotifyTest [小女] - 外卖送到没?[false] 15:08:58.235 guizy.WaitNotifyTest [小女] - 没外卖,先歇会! 15:08:59.232 guizy.WaitNotifyTest [送外卖的] - 送外卖的来咯~ 15:08:59.233 guizy.WaitNotifyTest [小女] - 外卖来咯, 可以开始干活了 15:09:00.233 guizy.WaitNotifyTest [送烟的] - 送烟的来咯~ 15:09:00.234 guizy.WaitNotifyTest [小南] - 烟来咯, 可以开始干活了
四、同步模式之顺序控制 (案例)· 假如有两个线程, 线程A打印1, 线程B打印2. 要求: 程序先打印2, 再打印1 1、Wait/Notify版本实现· 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @Slf4j(topic = "guizy.SyncPrintWaitTest") public class SyncPrintWaitTest { public static final Object lock = new Object (); public static boolean t2Runned = false ; public static void main (String[] args) { Thread t1 = new Thread (() -> { synchronized (lock) { while (!t2Runned) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("1" ); } }, "t1" ); Thread t2 = new Thread (() -> { synchronized (lock) { log.debug("2" ); t2Runned = true ; lock.notify(); } }, "t2" ); t1.start(); t2.start(); } }
2、使用ReentrantLock的await/signal· 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 @Slf4j(topic = "guizy.SyncPrintWaitTest") public class SyncPrintWaitTest { public static final ReentrantLock lock = new ReentrantLock (); public static Condition condition = lock.newCondition(); public static boolean t2Runned = false ; public static void main (String[] args) { Thread t1 = new Thread (() -> { lock.lock(); try { while (!t2Runned) { try { condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("1" ); } finally { lock.unlock(); } }, "t1" ); Thread t2 = new Thread (() -> { lock.lock(); try { log.debug("2" ); t2Runned = true ; condition.signal(); } finally { lock.unlock(); } }, "t2" ); t1.start(); t2.start(); } }
3、使用LockSupport中的park/unpart· 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Slf4j(topic = "guizy.SyncPrintWaitTest") public class SyncPrintWaitTest { public static void main (String[] args) { Thread t1 = new Thread (() -> { LockSupport.park(); log.debug("1" ); }, "t1" ); t1.start(); new Thread (() -> { log.debug("2" ); LockSupport.unpark(t1); }, "t2" ).start(); } }
16:10:28.592 guizy.SyncPrintWaitTest [t2] - 2 16:10:28.595 guizy.SyncPrintWaitTest [t1] - 1
交替输出· 需求
线程1 输出 a 5次, 线程2 输出 b 5次, 线程3 输出 c 5次。现在要求输出 abcabcabcabcabcabc 1、wait/notify版本 · 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 @Slf4j(topic = "guizy.TestWaitNotify") public class TestWaitNotify { public static void main (String[] args) { WaitNotify waitNotify = new WaitNotify (1 , 5 ); new Thread (() -> { waitNotify.print("a" , 1 , 2 ); }, "a线程" ).start(); new Thread (() -> { waitNotify.print("b" , 2 , 3 ); }, "b线程" ).start(); new Thread (() -> { waitNotify.print("c" , 3 , 1 ); }, "c线程" ).start(); } } @Slf4j(topic = "guizy.WaitNotify") @Data @AllArgsConstructor class WaitNotify { private int flag; private int loopNumber; public void print (String str, int waitFlag, int nextFlag) { for (int i = 0 ; i < loopNumber; i++) { synchronized (this ) { while (waitFlag != this .flag) { try { this .wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.print(str); this .flag = nextFlag; this .notifyAll(); } } } }
2、await/signal版本 · 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 @Slf4j(topic = "guizy.TestWaitNotify") public class TestAwaitSignal { public static void main (String[] args) throws InterruptedException { AwaitSignal awaitSignal = new AwaitSignal (5 ); Condition a_condition = awaitSignal.newCondition(); Condition b_condition = awaitSignal.newCondition(); Condition c_condition = awaitSignal.newCondition(); new Thread (() -> { awaitSignal.print("a" , a_condition, b_condition); }, "a" ).start(); new Thread (() -> { awaitSignal.print("b" , b_condition, c_condition); }, "b" ).start(); new Thread (() -> { awaitSignal.print("c" , c_condition, a_condition); }, "c" ).start(); Thread.sleep(1000 ); System.out.println("==========开始=========" ); awaitSignal.lock(); try { a_condition.signal(); } finally { awaitSignal.unlock(); } } } class AwaitSignal extends ReentrantLock { private final int loopNumber; public AwaitSignal (int loopNumber) { this .loopNumber = loopNumber; } public void print (String str, Condition condition, Condition next) { for (int i = 0 ; i < loopNumber; i++) { lock(); try { try { condition.await(); System.out.print(str); next.signal(); } catch (InterruptedException e) { e.printStackTrace(); } } finally { unlock(); } } } }
3、LockSupport的park/unpark实现· 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 @Slf4j(topic = "guizy.TestWaitNotify") public class TestParkUnpark { static Thread a; static Thread b; static Thread c; public static void main (String[] args) { ParkUnpark parkUnpark = new ParkUnpark (5 ); a = new Thread (() -> { parkUnpark.print("a" , b); }, "a" ); b = new Thread (() -> { parkUnpark.print("b" , c); }, "b" ); c = new Thread (() -> { parkUnpark.print("c" , a); }, "c" ); a.start(); b.start(); c.start(); LockSupport.unpark(a); } } class ParkUnpark { private final int loopNumber; public ParkUnpark (int loopNumber) { this .loopNumber = loopNumber; } public void print (String str, Thread nextThread) { for (int i = 0 ; i < loopNumber; i++) { LockSupport.park(); System.out.print(str); LockSupport.unpark(nextThread); } } }
五、本章小结·