什么是死锁
-
什么是死锁:
发生在并发中,两者互不相让,互相持有对方所需要的资源,又不主动释放,导致程序卡死。 -
死锁的影响
死锁的影响在不同的系统中不同的。在数据库中就是可以检测并且放弃事务的。但是JVM无法自动处理死锁。
死锁发生的几率不高但是危害大,压力测试无法找出所有的潜在死锁。
发生死锁的条件
- 互斥条件
- 请求与保持条件
- 不剥夺条件
- 循环等地条件。
图解Java并发设计认为三点
- 存在多个资源块
- 线程持有某个资源块时还希望获得其他的资源块
- 获取资源的顺序并不固定。
死锁的修复
可以采用ThreadMXBean
这一工具类。
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null && deadlockedThreads.length>0){
for (int i = 0; i < deadlockedThreads.length; i++) {
ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);
System.out.println("find "+threadInfo.getThreadName());
}
}
实际工程中如何避免死锁
- 线上问题都需要防患于未然,无法线上及时的修复
- 首先需要保存现场,保存堆栈的信息,然后重启服务器等。首先保证用户的体验。
- 暂时保证线上代码的安全, 利用保存的堆栈信息等,排查死锁,修改代码。
修复策略:
- 避免策略:哲学家就餐,设置一致的锁的获取顺序。
- 检测与恢复策略:每隔一段时间检测是否有死锁,如果有就剥夺某一个资源,打开死锁。
- 鸵鸟策略:直到发生死锁,再进行修复。
以哲学家就餐问题为例
public class test {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
int sum = 5;
Chopstick[] chopsticks = new Chopstick[sum];
for (int i = 0; i < sum; i++) {
chopsticks[i] = new Chopstick();
}
for (int i = 0; i < sum; i++) {
exec.execute(new Philosopher(chopsticks[i], chopsticks[(i + 1) % sum]));
}
}
}
// 筷子
class Chopstick {
public Chopstick() {
}
}
class Philosopher implements Runnable {
private Chopstick left;
private Chopstick right;
public Philosopher(Chopstick left, Chopstick right) {
this.left = left;
this.right = right;
}
@Override
public void run() {
try {
while (true) {
Thread.sleep(100);//思考一段时间
System.out.println(Thread.currentThread().getName() + " " + "Think");
synchronized (left) {
System.out.println(Thread.currentThread().getName() + " " + "left");
synchronized (right) {
System.out.println(Thread.currentThread().getName() + " " + "right");
Thread.sleep(1000);//进餐一段时间
}
}
}
}
catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
解决方案
- 服务员检查(避免策略)
- 改变一个哲学的拿起叉子的顺序, 给叉子进行编号,每次拿起小的。
- 餐票解决,最多n-1个餐票
- 检测与恢复策略,发生死锁之后检测到死锁,要求一个线程释放资源。
- 检测与恢复策略:逐个终止线程,直到死锁消除;资源抢占。
终止顺序:1. 优先级,前台交互还是后台处理;2. 已占用资源,还需要的资源。3. 运行时间。
线程回退几步,但是可能导致线程饥饿。
有效避免的方法:
8个经验:
- 设置超时时间:
tryLock(1000,TimeUnit.MILLISECONDS)
。synchronized无法尝试锁。获取锁失败再尝试发邮件,报警等。 - 多使用并发类,不要自己设置锁。
ConcurrentHashMap
,ConcurrentLinkedQueue
,AtomicBoolean
- 尽量较低锁的使用粒度和临界区。
- 如果可以使用同步代码块优先使用。
- 给线程起有意义的名字。
- 避免锁的嵌套。
- 分配资源前考虑能不能收回,银行家算法
- 不要几个功能用同一把锁。
其他活跃性问题
活锁
持有锁之后一段时间放开锁,再重新获得锁。可能正好还是卡死,编程活锁。
活锁的特点是线程没有被阻塞,但是没有进展。活锁还要持续消耗cpu资源。双方相互谦让导致谁都无法持有锁。
- 引入随机重试,解决活锁问题。
饥饿
线程需要资源但是无法得到资源。
优先级的设置需要注意。
面试问题
- 实际中的死锁,线程需要持有多个锁的场合,锁的嵌套。
- 如何定位死锁,ThreadMXBean找到死锁。jstack类。
- 解决死锁的策略。