死锁
死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
产生条件
java 死锁产生的四个必要条件:
1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。
死锁示例
有两个共享资源object1,object2,我们知道每个对象内部都有一个监视器锁,当一个线程占有资源 object1,保持不释放,并申请资源 object2;而另一个线程占有资源 object2,保持不释放,并申请资源 object1;这样就会导致两个线程一直在循环等待,产生死锁。
/**
* ClassName: MyDeadLock <br/>
* Function: 死锁示例<br/>
*
* @author gary.liu
* @date 2017/6/24
*/
public class MyDeadLock {
private static Object object1 = new Object();
private static Object object2 = new Object();
private static CountDownLatch countDownLatch = new CountDownLatch(1);
static class Worker implements Runnable {
public void run() {
synchronized (object1) {
System.out.println("Worker has object1 lock!");
/**
*
* 不睡一会的话,由于两个线程的运行时间可能不是同时的,就会有一个先运行,获取两个资源后释放,一个后运行,此时可能不会产生死锁;
* 如果线程几乎同时开始运行,则就有可能产生死锁,加这个 sleep 时间就是让死锁产生的更明显.
*
* 为了证明上面的说法,使用 CountDownLatch 让两个线程同时运行来看看结果
*
*/
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object2) {
//由于产生死锁,无法输出
System.out.println("Worker has object2 lock!");
}
}
}
}
static class Boss implements Runnable {
public void run() {
synchronized (object2) {
System.out.println("Boss has object2 lock!");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object1) {
//由于产生死锁,无法输出
System.out.println("Boss has object1 lock!");
}
}
}
}
public static void main(String[] args) throws Exception{
Thread worker = new Thread(new Worker());
Thread boss = new Thread(new Boss());
worker.start();
boss.start();
}
}
运行结果
Worker has object1 lock!
Boss has object2 lock!
//两个线程一直在循环等待获取对方资源,产生死锁,程序还一直在运行,但没有内容输出了
代码中的注释:
不睡一会的话,由于两个线程的运行时间可能不是同时的,就会有一个先运行,一个后运行,此时可能不会产生死锁;如果线程几乎同时开始运行,则就有可能产生死锁,加这个等待时间就是让死锁更有机会产生. 为了证明上面的说法,使用 CountDownLatch 让两个线程同时运行来看看结果,不用 sleep 一会,基本每次都会产生死锁。
两个线程同时运行,加大产生死锁的机会
public class MyDeadLock {
private static Object object1 = new Object();
private static Object object2 = new Object();
private static CountDownLatch countDownLatch = new CountDownLatch(1);
static class Worker implements Runnable {
public void run() {
synchronized (object1) {
try {
countDownLatch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
System.out.println("Worker has object1 lock!");
synchronized (object2) {
//由于产生死锁,无法输出
System.out.println("Worker has object2 lock!");
}
}
}
}
static class Boss implements Runnable {
public void run() {
synchronized (object2) {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Boss has object2 lock!");
synchronized (object1) {
//由于产生死锁,无法输出
System.out.println("Boss has object1 lock!");
}
}
}
}
public static void main(String[] args) throws Exception{
Thread worker = new Thread(new Worker());
Thread boss = new Thread(new Boss());
//因为用countDownLatch.await();两个线程被阻塞了
worker.start();
boss.start();
//更清楚的看到 worker,boss 线程被阻塞
Thread.sleep(3000);
//之后两个线程开始同时继续执行,基本每次都会产生死锁
countDownLatch.countDown();
}
}
分析死锁
1、jps
查看程序进程 id,即 pid
2、jstack pid
打印堆栈信息,可以从堆栈信息中看到 Thread-1 和 Thread-0 在互相等待对方释放资源。
具体分析参考:Java多线程7:死锁
或者用 jdk 自带的命令 jvisualvm
工具,对程序的运行进行监控,可以看到线程监控中已经检测到死锁,如下图。
避免死锁
破坏上面产生死锁的四个必要条件
遵循以下原则有助于规避死锁:
1、只在必要的最短时间内持有锁,考虑使用同步语句块代替整个同步方法;
2、尽量编写不在同一时刻需要持有多个锁的代码,如果不可避免,则确保线程持有第二个锁的时间尽量短暂;
3、创建和使用一个大锁来代替若干小锁,并把这个锁用于互斥,而不是用作单个对象的对象级别锁;
4、破坏循环等待条件,可以使用 Lock 类中的 tryLock 方法去尝试获取锁,这个方法可以指定一个超时时限,在等待超过该时限之后变回返回一个失败信息;也可以使用信号量,指定去获取的超时时间。