扫盲贴:死锁活锁饥饿
Posted 架构师之巅
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了扫盲贴:死锁活锁饥饿相关的知识,希望对你有一定的参考价值。
在安全性与活跃性之间通常存在着某种制衡,我们使用加锁机制来保证线程安全,但如果过度使用加锁,则可能导致死锁。
死锁
在业界有一个经典的哲学家进餐问题很好的描述了死锁状况。5个哲学家去吃中餐,他们有5根筷子,每两个人中间放一根筷子,哲学家们时而思考,时而进餐。每个人需要一双筷子才能吃到东西,并在吃完东西后,将筷子放回原处,继续思考。有些筷子管理算法能让每个人都能吃到东西,例如:一个哲学家会尝试获得相邻的两根筷子,如果其中一根筷子被另一个哲学家使用,他就会放弃手中获得那根筷子,继续等待几分钟之后再次尝试;但是有些管理算法会导致所有哲学家饿死,例如:每个人都死死抓住自己的筷子,然后等待另一根筷子,这将产生死锁。
当一个线程永远的持有一个锁,并且其他线程都尝试获得这个锁时,那么它们将永远被阻塞,在线程A持有锁M,又在尝试获取锁,同时线程B持有锁L ,又在尝试锁M ,那么线程A和线程B都将永远的等待下去,这种情况是最简单的死锁形式。
在数据库的设计中考虑了检测死锁以及从死锁中恢复,在执行一个事务时可能需要多个锁,并一直持有这些锁直到事务提交,因此在两个事务之间很可能发生死锁,但事实上这种情况并不常见,原因是数据库服务器检测到死锁时,会选择一个牺牲者,放弃这个事务,作为牺牲者的事务会释放锁,从而使其他事务正常执行,当其他事务都完成的时候,程序会重新启动这个牺牲者继续完成事务。
显然JVM解决死锁问题方面没有数据库那样强大,当Java线程发生死锁的时候,这些线程将再也无法继续使用了。
1、锁顺序导致死锁
我们来看一段代码
1public class LockDemo {
2 private Object left = new Object();
3 private Object right = new Object();
4
5 public void leftRight(){
6 synchronized (left){
7 synchronized (right){
8 //执行相关业务代码
9 dosomthing();
10 }
11 }
12 }
13
14
15 public void rightLeft(){
16
17 synchronized (right){
18 synchronized (left){
19 //执行相关业务代码
20 dosomthing();
21 }
22 }
23 }
24}
leftRight 和rightLeft方法分别获得left和right 锁,如果一个线程调用leftRight方法,并且同时另外一个线程调用了rightLeft方法,那么会发生死锁;
2、动态的锁顺序死锁
有时候通过程序代码,并不能清楚的知道是否通过锁顺序来避免死锁的发生。我们看下面一段代码,他将资金从一个账户转入到另一个账户,在开始转账之前需要获得这两个Account对象的锁。
1 public void transfer(Account from,Account to,Amount amount){
2 synchronized (from){
3 synchronized (to){
4 //... 此处省略一些校验信息
5 //from 账户扣钱
6 from.debit(amount);
7
8 //to 账户加钱
9 to.credit(amount);
10 }
11 }
12 }
这段代码如何发生死锁呢?如果两个线程同时调用transfer ,其中一个线程从X向Y转账,另外一个线程从Y向X转账,那么将会发生死锁。
1A: transfer(youAccount,myAccount,10);
2B: transfer(myAccount,youAccount,20);
3、如何避免死锁
3.1 只加一个锁
如果一个程序每次最多只获取一个锁,那么就不会产生死锁,当然这种情况不现实,如果能避免每次获取多个锁,则可以省去很多工作。如果必须获取多个锁,在设计的时候需要考虑锁的顺序,并且尽量减少加锁的交互数量;
3.2 支持定时的锁
我们可以使用AQS定制一种定时的锁,如果没有获得锁,我们设置一个超时时限,在等待超时时限后,我们返回一个timeout失败信息,如果超时时限比获取锁的时间要长很多,可以自己重新尝试获取锁的控制权。
饥饿
当线程由于无法访问它所需要的资源而不能继续执行的时候,就发生了“饥饿”。引发饥饿的最常见的资源就是CPU时钟周期。如果在JAVA程序中对线程优先级使用不当,或者在持有锁时执行一些无法结束的结构(例如:无限循环,或者无限制的等待某个资源),那么就会导致饥饿,因为其他线程无法获取这个锁。
活锁
活锁(Livelock)是另一种形式的话跃性问题,该问题尽管不会阻塞线程,但也不能继续执行,因为线程将不断重复执行相同的操作,而且总会失败。活锁通常发生在处理事务消息的应用程序中:如果不能成功地处理某个消息,那么消息处理机制将回滚整个事务,并将它重新放到队列的开头,线程将不断重复执行相同的操作,而且总会失败。
当多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都无法继续执行时,就发生了活锁。这就像两个过于礼貌的人在半路上面对面地相遇:他们彼此都让出对方的路,然而又在另一条路上相遇了。因此他们就这样反复地避让下去。
要解决这种活锁问题,需要在重试机制中引人随机性。例如:在网络上,如果两台机器尝试使用相同的载波来发送数据包,那么这些数据包将会发生冲突,这两台机器检测到了冲突后都选择了1秒后重发,那么冲突就会永远进行下去,如果引入随机时间进行重试,那么将避免冲突,从而有效的避免活锁。
以上是关于扫盲贴:死锁活锁饥饿的主要内容,如果未能解决你的问题,请参考以下文章