学 Java 多线程迟早会遇到死锁这个词。我第一次听到的时候脑子里冒出来的画面是代码卡在那里不动了点哪里都没反应。后来自己写了代码试了一下发现还真是这么回事。这篇依然是我的学习笔记。可能有些地方说得不准确但我尽量用我自己能理解的方式讲清楚。死锁到底是个啥简单说就是两个或两个以上的线程互相等对方手里的资源结果谁都等不到卡死了。就像两个人过独木桥走到中间谁也不让谁两个人都过不去。区别是现实里你俩可以商量一下或者其中一个人退回去。但在程序里如果没有外部的力量干预这几个线程就会永远卡在那里。死锁发生的四个条件——缺一个都不行我查了一圈资料所有讲死锁的地方都会提到这四个条件。它们得同时满足才会发生死锁。条件一资源一次只能一个人用这个叫互斥条件。意思就是某个资源比如一把锁同一时间只能被一个线程拿着。别人想用等着吧。其实这跟锁的本质有关系——锁本来就是来保证互斥的不然锁就没意义了。所以这个条件是破坏不了的。条件二拿着一个不放手还想要别人的这个叫请求与保持。意思是线程手里已经拿了一个资源它想再拿另一个资源。另一个资源被别的线程占着拿不到它就等着——但是等的时候它手里已经有的那个资源也不放。这就像你占着一个充电宝又想去拿别人手里的充电线。别人不给你线你就等着但你已经占着的充电宝也不给出去。条件三别人不能硬抢这个叫不剥夺条件。就是说一个线程拿着的资源在它用完主动释放之前其他线程不能强行抢走。还是刚才充电宝的例子——你手里那个充电宝别人不能直接从你手里夺走。只能等你自己用完了放回去。条件四绕成一个圈这个叫循环等待。意思就是几个线程互相等待——A 等 BB 等 CC 等 A绕成了一个圈。写个死锁的代码看看说再多不如写个代码跑一下。我看的最多的例子就是两个线程互相拿对方的锁。publicclassDeadlockExample{privatestaticfinalObjectresource1newObject();privatestaticfinalObjectresource2newObject();publicstaticvoidmain(String[]args){ThreadthreadAnewThread(()-{synchronized(resource1){System.out.println(线程 A 拿到了资源 1);try{Thread.sleep(100);}catch(InterruptedExceptione){e.printStackTrace();}synchronized(resource2){System.out.println(线程 A 拿到了资源 2);}}});ThreadthreadBnewThread(()-{synchronized(resource2){System.out.println(线程 B 拿到了资源 2);try{Thread.sleep(100);}catch(InterruptedExceptione){e.printStackTrace();}synchronized(resource1){System.out.println(线程 B 拿到了资源 1);}}});threadA.start();threadB.start();}}运行一下看看输出。正常情况下你会看到线程 A 拿到了资源 1 线程 B 拿到了资源 2然后程序就卡住了。因为线程 A 等着资源 2被 B 拿着线程 B 等着资源 1被 A 拿着。两个人互相等谁也不让谁。Thread.sleep(100)那一行看着不起眼但它很重要——它让两个线程拿完第一个资源之后停顿了一下给对面留下了拿第二个资源的时间。没有这个停顿死锁反而不一定触发。我第一次跑这个代码的时候忘了加 sleep结果有时候死锁有时候不死锁排查了半天才反应过来是怎么回事。怎么解决死锁——破坏条件就行了前面说了四个条件要同时满足才会死锁。所以想解决破坏其中一个就行。最常用的做法按固定顺序拿资源这个是破坏循环等待条件。说白了就是所有线程照着同一个顺序去拿资源。比如刚才的代码两个线程拿锁的顺序是反的——A 先拿资源1再拿资源2B 先拿资源2再拿资源1。这就是问题的根源。如果规定必须先拿资源1再拿资源2。那 A 拿了资源1之后B 就等资源1被释放不会出现互相等的环。代码改起来也很简单把线程 B 里拿锁的顺序调一下就行ThreadthreadBnewThread(()-{synchronized(resource1){// 先拿资源1System.out.println(线程 B 拿到了资源 1);try{Thread.sleep(100);}catch(InterruptedExceptione){e.printStackTrace();}synchronized(resource2){// 再拿资源2System.out.println(线程 B 拿到了资源 2);}}});这样就不会死锁了。这个方法最简单、最可靠也是实际项目里用得最多的。其他几种方法——了解就行一次性申请所有资源线程启动之前把它需要的所有资源全申请了少一个都不执行。这个方法的缺点是资源利用率太低——你占了两个资源但其实可能只用了一个另一个别人想用也用不了。拿不到就释放线程去申请新资源的时候如果被拒绝了就把自己已经有的资源也放了等一会儿再重新试。可以实现但代码写起来比较麻烦而且频繁释放和重新获取会影响性能。Java 里比较实用的做法用超时机制实际写代码的时候还有一个更实用的办法——用ReentrantLock的tryLock方法。synchronized是拿不到锁就一直等没有退路。但tryLock可以设置一个超时时间超过这个时间还没拿到线程就放弃做其他事情或者重试。ReentrantLocklocknewReentrantLock();if(lock.tryLock(2,TimeUnit.SECONDS)){// 拿到锁了干该干的事}else{// 超时了没拿到做别的处理}这种带超时的机制在实际开发中很常用。它让线程有了退路不会傻等到天荒地老。如果线上出了死锁怎么查这也是让我觉得 Java 挺神奇的一个地方——JVM 能自己检测到死锁。如果怀疑线上程序因为死锁卡住了可以用 JDK 自带的jstack命令jstack 进程ID输出日志的末尾会直接告诉你 “Found one Java-level deadlock”还会列出具体是哪个线程在等哪个资源。我第一次在测试环境试了一下这个命令看到死锁被 JVM 准确地打印出来的时候还挺震撼的。原来 Java 自己就在盯着这件事。学完的一点感受死锁这个东西说起来好像挺复杂的——四个条件、各种解决策略、一堆专业术语。但你细想一下其实核心就是一个字等。线程 A 等 BB 等 A两个人都等就卡死了。知道这个了再去看那四个条件、各种解决方案就都有了一个落脚的地方——不管怎么做本质就是打破这个等的链条。这篇也是边学边写的。如果有哪里理解得不对欢迎指出来我改。写这个例子的时候我自己也踩了坑——忘了加 sleep 导致死锁没触发还以为是代码写错了。后来才意识到并发编程里很多问题就是这样不是每次都出现出现了又不好复现这才是它最让人头疼的地方。