这是并发编程系列的第八篇文章。上一篇介绍了任务的取消及关闭,这一篇说一下死锁的问题。
哲学家问题
一提到死锁,很多人都会想到哲学家问题。假设有5个哲学家,围坐在一张圆桌旁,哲学家只做两件事,吃饭和思考。吃饭的时候需要使用两只筷子,但是每个哲学家面前只放了一支筷子,如果想吃饭,必须借用旁边哲学家的筷子。
这个时候如何管理每个哲学家吃饭与思考的时机和顺序就变得很重要。如果每个哲学家,拿起自己面前的筷子的时候,发现旁边的筷子不可用时,并不是释放手里筷子,而是死等旁边的筷子,就会发生死锁。所有哲学家都会因为等待对方的筷子而饿死。
死锁抽象模型
多个线程相互持有彼此正在等待的锁而又不释放自己已经持有的锁时就会发生死锁现象。对于上面的哲学家问题,筷子就是锁,而每位哲学家就是一个线程。
如果非要对死锁的场景做个简单的分类的话,那么大概可以分为如下几类。
锁顺序死锁
如果一个线程需要持有两把锁才能干活。比如线程A
先拿到1
号锁,然后再拿2
号锁,而这个时候线程B
抢先持有了2
号锁,而等待持有1
号锁。这个时候将造成死锁,造成这种死锁的原因是因为两个线程获取锁的顺序错乱了,如果都是按照先拿1
号锁,在拿2
号锁的顺序执行,就不会发生死锁情况。
资源死锁
假设一个任务需要对两个数据库进行连接,两个数据库的连接对象都从对应的数据库连接池中获取。
当线程A
持有数据库1
的连接,等待数据库2
的连接,而线程B
持有数据库2
的连接,而等待数据库1
的链接,如果任意一个数据库链接池资源不足,那么也将发生死锁现象。
线程饥饿
还有一种情况也会发生线程阻塞,假设我们定义了只有一个线程的线程池,但是我们提交了2个任务,第一个任务依赖于第二任务,但是因为第二个任务进入了等待队列(只有一个线程执行任务)。所以整个程序将会被阻塞住。
如何避免死锁
通过上面对死锁问题的产生原因进行分析,我们大概可以想到有那么几种方式可以避免死锁。
第一种方式就是避免持有多个锁,但是这种情况只适合比较简单的业务场景。
第二种方式就是使用带有超时时间的显示锁Lock,在规定的时间内获取不到相应的锁资源时则自动释放已经持有的锁资源,这样就可以避免长期持有锁而形成死锁。
结束
死锁问题很难被发现,即便你经历了严格的测试。死锁问题往往发生在线上高并发的场景下。所以各位在写并发程序的时候,一定要仔细分析业务需求及自己实现的代码逻辑。
推荐阅读
1. Java并发编程那些事儿(一) ——任务与线程
2. Java8的Stream流真香,没体验过的永远不知道
3. Awk这件上古神兵你会用了吗
4. 手把手教你搭建一套ELK日志搜索运维平台