一、死锁
死锁产生的四个必要条件
- 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
- 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
- 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
死锁示例
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
- t1 线程获得A对象锁,接下来想获取 B对象的锁
- t2 线程获得B对象锁,接下来想获取 A对象的锁
import lombok.extern.slf4j.Slf4j; import static site.weiyikai.thread.utils.Sleeper.sleep; /** * @author xiaowei * @date 2022-10-24 * @description 死锁 **/ @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(); } }
运行结果
21:51:08.966 c.TestDeadLock [t2] - lock B 21:51:08.966 c.TestDeadLock [t1] - lock A // 无限等待
二、定位死锁
检测死锁可以使用jconsole
工具,或者使用jps定位进程id,再用jstack
定位死锁。
2.1 jconsole工具检测死锁
(1)选择要监测死锁的进程
(2)选择线程->点击下方检测死锁按钮
(3)产生死锁的线程和信息
2.2 通过jstack
命令定位死锁
(1)通过jps
命令定位进程id
D:\Codes\idea\thread_demo>jps 7456 JConsole 9156 TestDeadLock 1128 Launcher 12264 Jps
(2)通过 jstack
命令定位死锁
D:\Codes\idea\thread_demo>jstack 9156
运行部分结果
............... Java stack information for the threads listed above: =================================================== "t2": at com.lilinchao.concurrent.demo_03.TestDeadLock.lambda$test1$1(TestDeadLock.java:37) - waiting to lock <0x00000000d7f90678> (a java.lang.Object) - locked <0x00000000d7f90688> (a java.lang.Object) at com.lilinchao.concurrent.demo_03.TestDeadLock$$Lambda$2/1416233903.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) "t1": at com.lilinchao.concurrent.demo_03.TestDeadLock.lambda$test1$0(TestDeadLock.java:26) - waiting to lock <0x00000000d7f90688> (a java.lang.Object) - locked <0x00000000d7f90678> (a java.lang.Object) at com.lilinchao.concurrent.demo_03.TestDeadLock$$Lambda$1/787387795.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) Found 1 deadlock.
- 避免死锁要注意加锁顺序
- 另外如果由于某个线程进入了死循环,导致其它线程一直等待
对于这种情况 linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用top -Hp
进程id 来定位是哪个线程,最后再用 jstack 排查
三、哲学家就餐问题
有五位哲学家,围坐在圆桌旁。
- 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
- 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
- 如果筷子被身边的人拿着,自己就得等待
import site.weiyikai.concurrent.utils.Sleeper; import lombok.extern.slf4j.Slf4j; /** * Created by xiaowei * Date 2022/10/24 * Description 哲学家就餐问题 */ public class Test05 { 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) { // 尝试获得左手筷子 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 + '}'; } }
运行结果
22:54:29.039 c.Philosopher [赫拉克利特] - eating... 22:54:29.039 c.Philosopher [苏格拉底] - eating... 22:54:29.544 c.Philosopher [亚里士多德] - eating... 22:54:29.544 c.Philosopher [阿基米德] - eating... 22:54:30.045 c.Philosopher [阿基米德] - eating... 22:54:30.545 c.Philosopher [赫拉克利特] - eating... 22:54:31.046 c.Philosopher [亚里士多德] - eating... // 无限等待
使用 jconsole 检测死锁
线程各自持有各自的资源无法释放。
这种线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题,除了死锁以外,还有活锁和饥饿者两种情况。
四、活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束。
活锁是一种情景,当两个或多个线程不停地尝试执行某个操作,但它们的执行被彼此的操作阻塞时,就会出现活锁。例如,在一个多人游戏中,两个角色相遇,他们的行动互相阻止对方移动,最终引发活锁。
在这种情况下,两个线程可以一直卡在互相阻止对方的操作上,导致无法结束程序。解决活锁的方法是引入一些调度机制,在两个线程中有一方先停止操作,让另一方继续执行,然后再轮到其它方停止操作。这样,两个线程可以在不互相阻塞的时候完成任务,从而避免了活锁的发生。
因此,在编程时,需要遵循一些规范,避免出现活锁的情况,以保证线程的正常运行。
import lombok.extern.slf4j.Slf4j; import static site.weiyikai.concurrent.utils.Sleeper.sleep; /** * @author xiaowei * @date 2022-10-24 * @description 活锁 **/ @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(); } }
运行结果
23:04:21.041 c.TestLiveLock [t1] - count: 9 23:04:21.041 c.TestLiveLock [t2] - count: 9 23:04:21.246 c.TestLiveLock [t2] - count: 10 23:04:21.246 c.TestLiveLock [t1] - count: 9 23:04:21.447 c.TestLiveLock [t2] - count: 10 23:04:21.447 c.TestLiveLock [t1] - count: 9 23:04:21.652 c.TestLiveLock [t2] - count: 10 ......................
分析
t1和t2两个线程,一个执行count--
,期望减到0的时候退出循环,一个执行count++
,期望加到20退出循环,但是永远不能都退出循环。
五、饥饿
很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题