更多 Java 并发编程方面的文章,请参见文集《Java 并发编程》
死锁 DeadLock
- 互斥:某个资源不能同时被多个线程占用
- 请求和保持:每个线程占有一些资源,同时等待另一些资源
- 不剥夺:如果某个资源被某个线程占用,其他线程不能抢占
- 循环等待
首先附上一段最简单的死锁代码:
public class DeadLockTesting {
public static void main(String args[]) {
Object obj1 = new Object();
Object obj2 = new Object();
DeadLockThread t1 = new DeadLockThread(obj1, obj2);
DeadLockThread t2 = new DeadLockThread(obj2, obj1);
t1.start();
t2.start();
}
}
class DeadLockThread extends Thread {
private Object obj1;
private Object obj2;
public DeadLockThread(Object obj1, Object obj2) {
this.obj1 = obj1;
this.obj2 = obj2;
}
public void run() {
synchronized (obj1) {
try {
Thread.sleep(1000);
synchronized (obj2) {
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
分析:
- 线程 t1 启动,获得资源 obj1 的锁,开始 sleep 1000ms
- t1 sleep 期间,线程 t2 启动,获得资源 obj2 的锁,开始 sleep 1000ms
- t1 sleep 结束,尝试获取资源 obj2 的锁,失败,等待
- t2 sleep 结束,尝试获取资源 obj1 的锁,失败,等待
- 死锁,主线程不会结束
死锁预防:
-
加锁顺序
确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。例如,在上述的代码中,都应该先锁obj1
,再锁obj2
-
加锁时限
在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁。 -
死锁检测
参见另一篇文章Java ThreadMXBean & 死锁检测
活锁 LiveLock
任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试、失败、尝试、失败。
活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
生活中的典型例子: 两个人在窄路相遇,同时向一个方向避让,然后又向另一个方向避让,如此反复。
计算机中的例子:两个线程发生了某些条件的碰撞后重新执行,那么如果再次尝试后依然发生了碰撞,长此下去就有可能发生活锁。
活锁的解决方法:
- 比如引入一些随机性。例如如果检测到冲突,那么就暂停随机的一定时间进行重试。这回大大减少碰撞的可能性。 典型的例子是以太网的CSMA/CD检测机制。
- 加入一定的重试次数也是有效的解决办法。
- 约定重试机制避免再次冲突。 例如自动驾驶的防碰撞系统(假想的例子),可以根据序列号约定检测到相撞风险时,序列号小的飞机朝上飞, 序列号大的飞机朝下飞。