多线程学习之常见死锁问题

多线程开发避不开锁,而锁又避不开死锁问题,所以弄清楚死锁问题才能开发出好的多线程程序。

死锁出现原因与解决方法

在多线程开发中,都是通过加锁来保证线程安全,但是过度的使用锁可能导致死锁。在数据库系统中有对死锁的检测并从死锁中恢复功能,一个事务可能会获取多个锁,当多个事务发生死锁,会选择牺牲一个事务,释放这个事务的所有锁,然后重新执行。而Java程序无法从死锁中恢复过来,因此在设计时一定要排除那些导致死锁出现的条件。

获取锁顺序造成死锁

而最简单的死锁是线程A持有资源M并想获取资源N,线程B持有资源N并想获取M,造成线程互相等待出现死锁。

造成这种死锁的原因是获取锁顺序不一致。所以解决方案是所有线程以固定的顺序来获得锁,那么在程序中就不会出现锁顺序死锁问题。

动态锁造成死锁

现在有一个转账程序,当A转账给B时,先获取A的锁在获取B锁,表面上似乎没有问题,但是如果同时又出现B转账给A,所以它会先锁B在锁A,造成死锁。这种不是代码层面的锁而是数据层面不容易发现。

要解决这种问题的根本办法还是加锁的顺序问题,现在的程序是固定的先锁定转账方在锁定收款方,由于用户身份的转变造成的加锁顺序不一致,可以在用户层面判断加锁顺序,比如根据用户id来判断加锁顺序而不是用户身份,这样所有的用户的加锁顺序就一致了,就解决了死锁问题。

相互引用造成死锁

一个synchronized修饰的方法内部调用了外部方法,而外部方法也是synchronized方法,虽然表面调用方法只有一个锁,实际上要执行这个方法却加了两个锁,实例如下图:

多线程学习之常见死锁问题_第1张图片

一个线程调用A对象的setA方法,另外一个线程调用B对象的println方法就有可能出现死锁情况,虽然表面上他们都没有加锁,但是两个方法实际上已经出现了顺序不一致的加锁方式。

如果在持有锁的时候调用外部方法,那么将出现活跃性问题,在这个外部方法中可能会获取其他锁(这可能会产生死锁),或者阻塞时间过长,导致其他线程无法及时获得当前被持有的锁。最好的办法是在调用某个方法时不需要持有锁,这种调用叫开放调用。上面示例的解决方法如下图:
多线程学习之常见死锁问题_第2张图片

通过改造两个方法在调用外部方法的时候都没有在持有锁,这样就不会出现死锁了。

资源死锁

除了加锁导致的死锁问题,还有一些并不是因为加锁而导致的,比如通过线程池和信号量来限制对资源的使用,这些被限制的行为可能导致资源死锁。

比如一个线程获取线程池A的线程并等待获取线程池B的线程,另外一个线程获取了线程池B的线程在等待获取线程池A的线程,也会出现资源死锁。

还有一种是线程饥饿死锁线程池的一个线程提交了一个任务到当前线程池也可能造成,比如线程池只有一个线程,这个线程往当前线程池提交了一个任务并且在等待执行结果,由于当前线程占用了线程池的资源,所以提交的任务会处于等待中,而当前线程也在阻塞等待提交的任务执行完成,最后造成死锁。

解决死锁

最好的结局方法是系统中的程序最多只获取一个锁,那么就不会产生死锁。如果要获取多个锁,那么就要严格限制获取锁的顺序,并形成获取锁的文档。

还有一种方法是定时锁,Lock类的tryLock可以指定超时时间,如果超时没获取就失败。

其他原因死锁

如果一个获取了某个锁的线程优先级很低或者处于一个死循环,那么就将导致其他线程不能获取到锁,也会出现死锁,所以在平时的开发中不推荐设置线程的优先级。

还有一种情况是活锁,提交的一个任务到队列,在执行的时候报错,可以会为了保护机制再次放到队列里面,等待下次执行,但是可能判断不完全使得这个任务不管运行多少次都是相同的结果,最终这个任务始终得不到最终的执行。

还有一种情况是当两个相互协作的线程,他们在发现对象在执行某些操作的时候会休息一下,等对方先执行,等一段时候后再次执行。这种协作机制会出现两个协作线程同时发现对方正在执行,同时休息相同的时间,然后当再次运行的时候发现对方仍然在执行,然后都会再次休眠,这样就会出现相互等待,任务一直执行不成功。这种解决方法是他们的休眠时间随机,这样就避免了大部分的阻塞情况。

总结

学习多线程开发避免不了锁,而学习锁也就避免死锁问题,造成死锁的大部分原因都是加锁的顺序问题,所以要么避免多次加锁,要么确定加锁的顺序,同时要小心动态锁和相互引用造成的加锁顺序问题。

同时还学习了一些不是加锁出现的问题,比如由于线程优先级太低一直不执行、线程池相互等待、协作线程相互等待等。了解这些情况的原理在开发中才能够避免。

Java程序员日常学习笔记,如理解有误欢迎各位交流讨论!
多线程学习之常见死锁问题_第3张图片

你可能感兴趣的:(多线程学习之常见死锁问题)