3.13 多把锁
多把不相干的锁
一间大屋子有两个功能:睡觉、学习,互不相干。
现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低
解决方法是准备多个房间(多个对象锁)
例如
public class TestMultiLock { public static void main(String[] args) { BigRoom bigRoom = new BigRoom(); new Thread(() -> { bigRoom.study(); },"小南").start(); new Thread(() -> { bigRoom.sleep(); },"小女").start(); } } @Slf4j(topic = "c.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); } } }
改进:
@Slf4j(topic = "c.BigRoom") class BigRoom { //使用多把锁,相当于把房间分成了两块,允许两个没有关联的动作同时进行 private final Object studyRoom = new Object(); private final Object bedRoom = new Object(); public void sleep() { synchronized (bedRoom) { log.debug("sleeping 2 小时"); Sleeper.sleep(2); } } public void study() { synchronized (studyRoom) { log.debug("study 1 小时"); Sleeper.sleep(1); } } }
将锁的粒度细分
- 好处,是可以增强并发度
- 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁
3.14 活跃性
3.14.1 死锁
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
t1 线程
获得A对象
锁,接下来想获取 B对象
的锁 。t2 线程
获得 B对象
锁,接下来想获取 A对象
的锁。
代码演示:
@Slf4j(topic = "c.TestDeadLock") public class TestDeadLock { public static void main(String[] args) { test1(); } private static void test1() { Object A = new Object(); Object B = new Object(); Thread t1 = new Thread(() -> { synchronized (A) { log.debug("lock A"); sleep(1); synchronized (B) { log.debug("lock B"); log.debug("操作..."); } } }, "t1"); Thread t2 = new Thread(() -> { synchronized (B) { log.debug("lock B"); sleep(0.5); synchronized (A) { log.debug("lock A"); log.debug("操作..."); } } }, "t2"); t1.start(); t2.start(); } }
结果:
15:13:19.339 c.TestDeadLock [t2] - lock B 15:13:19.339 c.TestDeadLock [t1] - lock A
3.14.2 定位死锁
检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:
- 使用jps+jstack定位死锁
F:\自学课程\底层课程\Java并发编程\concurrent>jps //查看正在运行的Java进程 11188 Launcher 17828 Jps 15192 TestDeadLock 19404 4684 RemoteMavenServer36 F:\自学课程\底层课程\Java并发编程\concurrent>jstack 15192//查看进程的详细信息 Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.192-b12 mixed mode): "DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x0000000002e13800 nid=0x428c waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE //以下是t2线程的详细信息 "t2" #12 prio=5 os_prio=0 tid=0x000000001ed35800 nid=0x290 waiting for monitor entry [0x000000001f37f000] java.lang.Thread.State: BLOCKED (on object monitor)//阻塞状态 at cn.itcast.n4.deadlock.TestDeadLock.lambda$test1$1(TestDeadLock.java:32) - waiting to lock <0x000000076ec5d160> (a java.lang.Object)//正在等待的锁... - locked <0x000000076ec5d170> (a java.lang.Object)//已经获得的锁 at cn.itcast.n4.deadlock.TestDeadLock$$Lambda$2/1792845110.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) //以下是t1线程的详细信息 "t1" #11 prio=5 os_prio=0 tid=0x000000001ed35000 nid=0xcd4 waiting for monitor entry [0x000000001f27f000] java.lang.Thread.State: BLOCKED (on object monitor) at cn.itcast.n4.deadlock.TestDeadLock.lambda$test1$0(TestDeadLock.java:21) - waiting to lock <0x000000076ec5d170> (a java.lang.Object) - locked <0x000000076ec5d160> (a java.lang.Object) at cn.itcast.n4.deadlock.TestDeadLock$$Lambda$1/897913732.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) //省略部分输出 Found one Java-level deadlock: //发现一个Java级别的死锁 ============================= "t2": waiting to lock monitor 0x0000000002f0c0c8 (object 0x000000076ec5d160, a java.lang.Object), which is held by "t1" "t1": waiting to lock monitor 0x0000000002f0b9e8 (object 0x000000076ec5d170, a java.lang.Object), which is held by "t2" Java stack information for the threads listed above: =================================================== "t2"://出现死锁的行号信息 at cn.itcast.n4.deadlock.TestDeadLock.lambda$test1$1(TestDeadLock.java:32) - waiting to lock <0x000000076ec5d160> (a java.lang.Object) - locked <0x000000076ec5d170> (a java.lang.Object) at cn.itcast.n4.deadlock.TestDeadLock$$Lambda$2/1792845110.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) "t1": at cn.itcast.n4.deadlock.TestDeadLock.lambda$test1$0(TestDeadLock.java:21) - waiting to lock <0x000000076ec5d170> (a java.lang.Object) - locked <0x000000076ec5d160> (a java.lang.Object) at cn.itcast.n4.deadlock.TestDeadLock$$Lambda$1/897913732.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) Found 1 deadlock.
- 使用Jconsole工具
- 避免死锁要注意加锁顺序
- 另外如果由于某个线程进入了死循环,导致其它线程一直等待。对于这种情况 linux 下可以通过 jps+jstack方式进行排查。
3.14.3 哲学家就餐问题
有五位哲学家,围坐在圆桌旁。
- 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
- 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
- 如果筷子被身边的人拿着,自己就得等待
//就餐测试类 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("阿基米德", c1, c5).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) { // 尝试获得左手筷子 synchronized (left) { // 尝试获得右手筷子 synchronized (right) { eat(); } } } } Random random = new Random(); 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 + '}'; } }
执行不多会,就执行不下去了
12:33:15.575 [苏格拉底] c.Philosopher - eating... 12:33:15.575 [亚里士多德] c.Philosopher - eating... 12:33:16.580 [阿基米德] c.Philosopher - eating... 12:33:17.580 [阿基米德] c.Philosopher - eating... // 卡在这里, 不向下运行
使用 jconsole 检测死锁,发现
------------------------------------------------------------------------- 名称: 阿基米德 状态: cn.itcast.Chopstick@1540e19d (筷子1) 上的BLOCKED, 拥有者: 苏格拉底 总阻止数: 2, 总等待数: 1 堆栈跟踪: cn.itcast.Philosopher.run(TestDinner.java:48) - 已锁定 cn.itcast.Chopstick@6d6f6e28 (筷子5) ------------------------------------------------------------------------- 名称: 苏格拉底 状态: cn.itcast.Chopstick@677327b6 (筷子2) 上的BLOCKED, 拥有者: 柏拉图 总阻止数: 2, 总等待数: 1 堆栈跟踪: cn.itcast.Philosopher.run(TestDinner.java:48) - 已锁定 cn.itcast.Chopstick@1540e19d (筷子1) ------------------------------------------------------------------------- 名称: 柏拉图 状态: cn.itcast.Chopstick@14ae5a5 (筷子3) 上的BLOCKED, 拥有者: 亚里士多德 总阻止数: 2, 总等待数: 0 堆栈跟踪: cn.itcast.Philosopher.run(TestDinner.java:48) - 已锁定 cn.itcast.Chopstick@677327b6 (筷子2) ------------------------------------------------------------------------- 名称: 亚里士多德 状态: cn.itcast.Chopstick@7f31245a (筷子4) 上的BLOCKED, 拥有者: 赫拉克利特 总阻止数: 1, 总等待数: 1 堆栈跟踪: cn.itcast.Philosopher.run(TestDinner.java:48) - 已锁定 cn.itcast.Chopstick@14ae5a5 (筷子3) ------------------------------------------------------------------------- 名称: 赫拉克利特 状态: cn.itcast.Chopstick@6d6f6e28 (筷子5) 上的BLOCKED, 拥有者: 阿基米德 总阻止数: 2, 总等待数: 0 堆栈跟踪: cn.itcast.Philosopher.run(TestDinner.java:48) - 已锁定 cn.itcast.Chopstick@7f31245a (筷子4)
这种线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题,除了死锁以外,还有活锁和饥饿者两种情况
3.14.4 活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如
@Slf4j(topic = "c.TestLiveLock") public class TestLiveLock { static volatile int count = 10; static final Object lock = new Object(); public static void main(String[] args) { new Thread(() -> { // 期望减到 0 退出循环 while (count > 0) { sleep(0.2); count--; log.debug("count: {}", count); } }, "t1").start(); new Thread(() -> { // 期望超过 20 退出循环 while (count < 20) { sleep(0.2); count++; log.debug("count: {}", count); } }, "t2").start(); } }
**解决办法:**让两个线程的执行时间交错,具体方案是通过增加随机睡眠时间。
3.14.5 饥饿
很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题。
下面我讲一下我遇到的一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题
顺序加锁的解决方案(线程1,2获取锁的顺序是相同的)
案例演示: