扫盲贴:死锁、活锁、饥饿

在安全性与活跃性之间通常存在着某种制衡,我们使用加锁机制来保证线程安全,但如果过度使用加锁,则可能导致死锁。

死锁

在业界有一个经典的哲学家进餐问题很好的描述了死锁状况。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 }131415 public void rightLeft(){1617 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秒后重发,那么冲突就会永远进行下去,如果引入随机时间进行重试,那么将避免冲突,从而有效的避免活锁。

本头条号团队成员由饿了么、阿里、蚂蚁金服等同事组成,另外可以关注V信公众号: jiagoushizhidian,可以了解最前沿的技术。


分享到:


相關文章: