死锁
什么是死锁
死锁是指当两个或多个线程在并发中互相持有对方所需要的资源且不释放,导致所有线程都无法继续前进,导致程序进入阻塞。
最简单的例子就是A线程持有锁1,需要锁2,而B线程持有锁2,需要锁1,这样两个线程都无法前进。
死锁的影响
当一组Java线程发生死锁时,这些线程就无法再使用了,根据线程完成工作的不同,可能造成程序完全停止,或者性能降低。JVM是无法自动处理,唯一恢复的方法就只有中止并重启它。
代码演示
/**
* 死锁演示
* @author samy
* @date 2019/9/27 14:35
*/
public class DeadLock {
static final Object object1 = new Object();
static final Object object2 = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
synchronized(object1){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object2){
System.out.println("thread1 run...");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized(object2){
synchronized (object1){
System.out.println("thread2 run...");
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("thread1 and thread2 end");
}
}
死锁的四个必要条件
互斥条件:存在临界区,同一时间只有一个线程能访问
请求与保持条件:在持有锁的同时,又请求另一把锁
不可剥夺条件:在持有锁时无法被释放
循环等待条件:即A等B,B等A这样的循环等待
定位死锁的方式
- jstack
使用jstack命令可以查询线程的堆栈信息和状态,如何发生死锁也可以被发现。
使用jps发现程序的pid,
然后使用jstack [pid]打印程序的线程情况,结果如下:
从图上信息可以发现线程0持有0x000000076bc91358锁,需要0x000000076bc91348锁,而线程1持有0x000000076bc91348锁,需要0x000000076bc91358锁。
并且在最后jstack已经打印了Found 1 deadlock。
- ThreadMxBean
/**
* 使用ThreadMXBean发现死锁
* @author samy
* @date 2019/9/28 14:41
*/
public class ThreadMXBeanDetection {
static final Object object1 = new Object();
static final Object object2 = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
synchronized(object1){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object2){
System.out.println("thread1 run...");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized(object2){
synchronized (object1){
System.out.println("thread2 run...");
}
}
});
thread1.start();
thread2.start();
Thread.sleep(1000);
/**
* 获取threadMXBean发现死锁的线程
*/
//获取发现死锁的工具对象
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
//发现死锁的线程pid
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
for (long deadlockedThread : deadlockedThreads){
//获取线程信息
ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThread);
System.out.println(threadInfo.getThreadName() + "发生死锁了");
}
}
}
可以在使用锁的地方添加ThreadMXBean来检测是否发生死锁,增加了代码的健壮性。
ThreadMXBean与jstack相比更加灵活。
发生死锁的修复策略
- 避免策略
可以自行固定获取锁的顺序,可以按找对象主键pid或者hash值采取不同的获取锁策略;另外也可以采用哲学家就餐的换手方案。
- 检测与恢复策略
进程终止:一段时间检测是否有死锁,如果存在死锁,可以逐个终止线程,让其释放持有的资源,直到死锁消除,但通常这样带来的危害很大;
资源抢占:也可以剥夺线程持有的资源,但不中断它,让它回退几步,这样就不用结束整个线程,成本比较低。
实际工程中如何避免死锁
- 设置超时时间
使用Lock的tryLock()方法,让其在一定时间内没有得到期望的资源就不在等待。
/**
* @author samy
* @date 2019/9/28 15:07
*/
public class TryLockDeadLock {
static final Lock lock1 = new ReentrantLock();
static final Lock lock2 = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
try {
if (lock1.tryLock(800,TimeUnit.MILLISECONDS)){
System.out.println("线程1拿到了锁1");
Thread.sleep(100);
try {
if (lock2.tryLock(800,TimeUnit.MILLISECONDS)){
try {
System.out.println("线程1拿到了锁2");
System.out.println("线程1拿到了两个锁");
}finally {
lock2.unlock();
}
}
}finally {
lock1.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread thread2 = new Thread(() -> {
try {
if (lock2.tryLock(3000,TimeUnit.MILLISECONDS)){
System.out.println("线程2拿到了锁2");
try {
if (lock1.tryLock(3000,TimeUnit.MILLISECONDS)){
try {
System.out.println("线程2拿到了锁1");
System.out.println("线程2拿到了两个锁");
}finally {
lock1.unlock();
}
}
}finally {
lock2.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("thread1 and thread2 end");
}
}
- 多使用并发类
ConcurrentHashMap,ConcurrentLinkedQueue和原子类十分有用
- 避免锁的嵌套