五、线程同步之死锁和活锁
死锁和活锁现象
死锁
死锁现象
死锁:两个或多个线程相互等待对方释放锁,则会出现死锁现象。java虚拟机没有检测,也没有采用措施来处理死锁情况,所以多线程编程是应该采取措施避免死锁的出现。一旦出现死锁,整个程序即不会发生任何异常,也不会给出任何提示,只是所有线程都处于堵塞状态。死锁情况如下图所示。
![](https://images2015.cnblogs.com/blog/1088097/201612/1088097-20161229110033789-1513004815.png)
下面代码中有两个对象作为锁,两个线程,线程1先持有A,请求B;线程2先持有B,请求A,导致两条线程互不相让,导致卡死状态。
public class Main {
static Object lockA = new Object();
static Object lockB = new Object();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lockA) {
System.out.println("第一个线程调用A");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB) {
System.out.println("第一个线程调用B");
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lockB) {
System.out.println("第二个线程调用B");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockA) {
System.out.println("第二个线程调用A");
}
}
}
}).start();
}
}
![](https://images2015.cnblogs.com/blog/1088097/201612/1088097-20161229110034273-388036840.png)
避免和打破死锁状态
形成死锁的条件
- 互斥条件:线程使用的资源必须至少有一个是不能共享的(至少有锁);
- 请求与保持条件:至少有一个线程必须持有一个资源并且正在等待获取一个当前被其它线程持有的资源(至少两个线程持有不同锁,又在等待对方持有锁);
- 非剥夺条件:分配资源不能从相应的线程中被强制剥夺(不能强行获取被其他线程持有锁);
- 循环等待条件:第一个线程等待其它线程,后者又在等待第一个线程(线程A等线程B;线程B等线程C;...;线程N等线程A。如此形成环路)。
死锁预防
- 加锁顺序
当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。这种方式是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁,但总有些时候是无法预知的。
- 加锁时限
在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁。 - 死锁检测(检测死锁的比如有jstack)
死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。每当一个线程获得了锁,会在线程和锁相关的数据结构中(map等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。检测到死锁之后:
一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁。一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。
打破死锁
这里不做讨论和验证,原则是形成死锁条件的四个必要条件打破任意一个条件,就可以打开死锁。
活锁
任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
活锁的几个例子
单一实体的活锁
例如线程从队列中拿出一个任务来执行,如果任务执行失败,那么将任务重新加入队列,继续执行。假设任务总是执行失败,或者某种依赖的条件总是不满足,那么线程一直在繁忙却没有任何结果。
协同导致的活锁
生活中的典型例子: 两个人在窄路相遇,同时向一个方向避让,然后又向另一个方向避让,如此反复。
通信中也有类似的例子,多个用户共享信道(最简单的例子是大家都用对讲机),同一时刻只能有一方发送信息。发送信号的用户会进行冲突检测, 如果发生冲突,就选择避让,然后再发送。 假设避让算法不合理,就导致每次发送,都冲突,避让后再发送,还是冲突。
计算机中的例子:两个线程发生了某些条件的碰撞后重新执行,那么如果再次尝试后依然发生了碰撞,长此下去就有可能发生活锁。
活锁的解决方法
解决协同活锁的一种方案是调整重试机制。
比如引入一些随机性。例如如果检测到冲突,那么就暂停随机的一定时间进行重试。这回大大减少碰撞的可能性。 典型的例子是以太网的CSMA/CD检测机制。
另外为了避免可能的死锁,适当加入一定的重试次数也是有效的解决办法。尽管这在业务上会引起一些复杂的逻辑处理。
比如约定重试机制避免再次冲突。 例如自动驾驶的防碰撞系统(假想的例子),可以根据序列号约定检测到相撞风险时,序列号小的飞机朝上飞, 序列号大的飞机朝下飞。