产生背景

简单的说:线程1 想要去拿一个由 线程2 持有的锁,由于synchronized 的锁是互斥锁,某一时刻只能被一个线程所持有,所以线程1 就拿不到锁。

死锁

死锁原因

是指两个两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
原因如下:

  1. 因为系统资源不足。
  2. 进程运行推进的顺序不合适,这种产生的最多。
  3. 资源分配不当。

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放,而该资源又被其他线程锁定,从而导致每一个线程都得等其它线程释放其锁定的资源,造成了所有线程都无法正常结束。

因为多线程访问共享资源,由于访问的顺序不当所造成的,通常是一个线程锁定了一个资源A,而又想去锁定资源B;在另一个线程中,锁定了资源B,而又想去锁定资源A以完成自身的操作,两个线程都想得到对方的资源,而不愿释放自己的资源,造成两个线程都在等待,而无法执行的情况。

产生背景: 在多线程环境下,争抢同是争抢对方资源(锁)就会产生该问题,即产生死锁。

java 死锁产生的四个必要条件

  1. 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
  2. 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
  3. 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
  4. 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

举列说明场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* 死锁
*/
public class SyncDeadLock {
private static Object locka = new Object();
private static Object lockb = new Object();

public static void main(String[] args) {
new SyncDeadLock().deadLock();
}

private void deadLock() {

Thread thread1 = new Thread(() -> {
synchronized (locka) {
try {
System.out.println(Thread.currentThread().getName() + " 拿到 lock-A!");
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + " 睡眠 500ms 后续继执行...!");
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 偿试获取 lock-B!");
synchronized (lockb) {
System.out.println(Thread.currentThread().getName() + " 已获得 lock-B!");
}
}
}, "thread1");

Thread thread2 = new Thread(() -> {
synchronized (lockb) {
try {
System.out.println(Thread.currentThread().getName() + " 拿到 lock-B!");
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + " 睡眠 500ms 后续继执行...!");
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 偿试获取 lock-A!");
synchronized (locka) {
System.out.println(Thread.currentThread().getName() + " 已获得 lock-A!");
}
}
}, "thread2");

thread1.start();
thread2.start();
}
}

结果

1
2
3
4
5
6
thread1 拿到 lock-A!
thread2 拿到 lock-B!
thread1 睡眠 500ms 后续继执行...!
thread1 偿试获取 lock-B!
thread2 睡眠 500ms 后续继执行...!
thread2 偿试获取 lock-A!

帮助理解

地上放着两个桶泡面,一个老坛酸菜,一个小鸡炖磨茹。两个人一个产品,一个测试,同时出发去抢老坛酸菜和小鸡炖磨茹,产品老坛酸菜,测试拿到小鸡炖磨茹,同一时刻,产品伸要去拽测试怀里的小鸡炖磨茹,测试伸手去拽产品的老坛酸菜,两个僵持不下,就死在那了,叫死锁。如果没有开发将他们各打一顿解救出来,它们奖无法推进下去。

死锁是因为多线程访问共享资源,由于访问的顺序不当所造成的,通常是一个线程锁定了一个资源A,而又想去锁定资源B;在另一个线程中,锁定了资源B,而又想去锁定资源A以完成自身的操作,两个线程都想得到对方的资源,而不愿释放自己的资源,造成两个线程都在等待,而无法执行的情况。

死锁预防

如果只使用一个锁就不会有死锁的问题,不过复杂场景下不太理实。

以确定的顺序获得锁

1
2
3
线程A ---> 锁定 A  ----> 偿试锁定 B
线程B ---> 锁定 A ----> 偿试锁定 B
这样就不会发生死锁

超时放弃

Lock 接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException方法,该方法可以按照固定时长等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。通过这种方式,也可以很有效地避免死锁。

信号量控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import java.util.Date;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

/**
* 信号量控制,解决死锁
*/
public class UnLockTest {
public static final Semaphore a1 = new Semaphore(1);
public static final Semaphore a2 = new Semaphore(1);

public static void main(String[] args) {
LockAa la = new LockAa();
new Thread(la).start();
LockBb lb = new LockBb();
new Thread(lb).start();
}
}
class LockAa implements Runnable {
public void run() {
try {
System.out.println(new Date().toString() + " LockA 开始执行");
while (true) {
if (UnLockTest.a1.tryAcquire(1, TimeUnit.SECONDS)) {
System.out.println(new Date().toString() + " LockA 锁住 obj1");
if (UnLockTest.a2.tryAcquire(1, TimeUnit.SECONDS)) {
System.out.println(new Date().toString() + " LockA 锁住 obj2");
Thread.sleep(60 * 1000); // do something
}else{
System.out.println(new Date().toString() + "LockA 锁 obj2 失败");
}
}else{
System.out.println(new Date().toString() + "LockA 锁 obj1 失败");
}
UnLockTest.a1.release(); // 释放
UnLockTest.a2.release();
Thread.sleep(1000); // 马上进行尝试,现实情况下do something是不确定的
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class LockBb implements Runnable {
public void run() {
try {
System.out.println(new Date().toString() + " LockB 开始执行");
while (true) {
if (UnLockTest.a2.tryAcquire(1, TimeUnit.SECONDS)) {
System.out.println(new Date().toString() + " LockB 锁住 obj2");
if (UnLockTest.a1.tryAcquire(1, TimeUnit.SECONDS)) {
System.out.println(new Date().toString() + " LockB 锁住 obj1");
Thread.sleep(60 * 1000); // do something
}else{
System.out.println(new Date().toString() + "LockB 锁 obj1 失败");
}
}else{
System.out.println(new Date().toString() + "LockB 锁 obj2 失败");
}
UnLockTest.a1.release(); // 释放
UnLockTest.a2.release();
Thread.sleep(10 * 1000); // 这里只是为了演示,所以tryAcquire只用1秒,而且B要给A让出能执行的时间,否则两个永远是死锁
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Sun Oct 10 12:15:25 CST 2018 LockA 锁住 obj1
Sun Oct 10 12:15:25 CST 2018 LockA 锁住 obj2
Sun Oct 10 12:15:43 CST 2018 LockB 锁住 obj2
Sun Oct 10 12:15:43 CST 2018 LockB 锁住 obj1
Sun Oct 10 12:16:26 CST 2018 LockA 锁住 obj1
Sun Oct 10 12:16:26 CST 2018 LockA 锁住 obj2
Sun Oct 10 12:16:53 CST 2018 LockB 锁住 obj2
Sun Oct 10 12:16:53 CST 2018 LockB 锁住 obj1
Sun Oct 10 12:17:27 CST 2018 LockA 锁住 obj1
Sun Oct 10 12:17:27 CST 2018 LockA 锁住 obj2
Sun Oct 10 12:18:03 CST 2018 LockB 锁住 obj2
Sun Oct 10 12:18:03 CST 2018 LockB 锁住 obj1
Sun Oct 10 12:18:28 CST 2018 LockA 锁住 obj1
Sun Oct 10 12:18:28 CST 2018 LockA 锁住 obj2
Sun Oct 10 12:19:13 CST 2018 LockB 锁住 obj2
Sun Oct 10 12:19:13 CST 2018 LockB 锁住 obj1