Java wait()/notify()/notifyAll/死锁

多线程编程中要特别注意可能发生死锁的场景,java多线程中与锁相关的几个方法有wait()/notify()/sleep()/yeild()等。
这里由一个死锁为题引出几个方法使用时应注意的问题。

线程生命周期

死锁案例:notify和notifyAll

先来看一段程序:

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
class PubSub {

private boolean flag;
private int count;

public PubSub() {
}

public synchronized void pub() {
try {
while (!flag) {
this.wait();
}
System.out.println("pub++++" + count);
flag = false;
this.notify();
}
catch (InterruptedException e) {
e.printStackTrace();
}
count++;

}

public synchronized void sub() {
try {
while (flag) {
this.wait();
}
System.out.println("sub----" + count);
flag = true;
this.notify();
}
catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
}

public class NotityAndNotifyAll extends Test {
public static void main(String[] args) {
final PubSub pb = new PubSub();
for (int j = 0; j < 100; j++) {
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 5; i++) {
pb.sub();
}
}
}).start();

new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 5; i++) {
pb.pub();
}
}
}).start();
}
}
}
  • 执行这段程序,发现大部分情况都不会打印1000条记录,会在中间卡住,发生了死锁。
  • 原因分析:
    • pub和sub都是同步方法,所以多个调用sub和sub的线程都会处于阻塞状态,等待一个正在运行的线程去唤醒他们。
    • notify方法只能唤醒一个线程,其它等待的线程仍然处于wait状态。假设调用sub方法的线程执行完后,这时如果唤醒的是一个sub方法的调度线程,那么while循环等于true,则此唤醒的线程也会处于等待状态,此时所有的线程都处于等待状态,那么也就没有了运行的线程来唤醒它们,这就发生了死锁。
  • 解决方法:
    • 使用notifyAll()
    • notifyAll方法来唤醒所有正在等待该锁的线程,那么所有的线程都会处于运行前的准备状态(就是sub方法执行完后,唤醒了所有等待该锁的状态,注:不是wait状态),那么此时,即使再次唤醒一个sub方法调度线程,while循环等于true,唤醒的线程再次处于等待状态,那么还会有其它的线程可以获得锁,进入运行状态。
  • notify与notifyAll区别
    • notifyAll使所有原来在该对象上等待被notify的线程统统退出wait的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。
    • notify他只是选择一个wait状态线程进行通知,并使它获得该对象上的锁,但不惊动其他同样在等待被该对象notify的线程们,当第一个线程运行完毕以后释放对象上的锁,此时如果该对象没有再次使用notify语句,即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,继续处在wait状态,直到这个对象发出一个notify或notifyAll,它们等待的是被notify或notifyAll,而不是锁。

Java sleep/notify/wait/yield

sleep和yield等并没有释放锁,而wait释放了锁。
只能在同步控制方法或同步块中调用wait()、notify()和notifyAll()。

使用notify和wait可以让多个线程间相互协作,以控制线程运行,以下示例为使用Controller对Monitor进行控制。

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
class Controller implements Runnable {
private Monitor monitor;

public Controller(Monitor monitor) {
this.monitor = monitor;
}

public void run() {
try {
System.out.println("Just waiting.");
TimeUnit.SECONDS.sleep(2);
monitor.gotMessage();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}

class Monitor implements Runnable {
private volatile boolean go = false;

public synchronized void gotMessage() throws InterruptedException {
go = true;
notifyAll();
}

public synchronized void watching() throws InterruptedException {
while (go == false) {
System.out.println(Thread.currentThread().getName() + " wait");
wait();
}
System.out.println(Thread.currentThread().getName() + " done.");
}

public void run() {
try {
watching();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public class Wait {
public static void main(String[] args) {
Monitor monitor = new Monitor();
Controller c = new Controller(monitor);
new Thread(c).start();

for (int i = 0; i < 5; i++)
new Thread(monitor, i + "").start();
}
}

通过Controller来控制条件变量go,开始时所有的Monitor均处于wait,等到Controller将go置为true后,所有Monitor被唤醒。
注意这里使用的是notifyAll,使用notify会发生死锁。

这个示例的本质是“忙等待”,等条件达到了再运行,它还有另一种写法。

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
class Controller implements Runnable {
private Monitor monitor;

public Controller(Monitor monitor) {
this.monitor = monitor;
}

public void run() {
try {
System.out.println("Just waiting.");
TimeUnit.SECONDS.sleep(2);
monitor.gotMessage();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}

class Monitor implements Runnable {
private volatile boolean go = false;

public void gotMessage() {
go = true;

}

public void watching() {
while (go == false) {
System.out.println(Thread.currentThread().getName() + " wait");
}
System.out.println(Thread.currentThread().getName() + " done.");
}

public void run() {
watching();
}
}

public class Wait {
public static void main(String[] args) {
Monitor monitor = new Monitor();
Controller c = new Controller(monitor);
new Thread(c).start();

for (int i = 0; i < 5; i++)
new Thread(monitor, i + "").start();
}
}

注意:

  • watching方法不再是同步方法,想想为什么?注意go其实为volatile类型,并不需要强制去同步。
  • goMessage方法不再需要notifyAll

其他死锁案例

  • synchronized与ReentrantLock读写锁

    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
    import java.util.concurrent.locks.ReentrantReadWriteLock;

    /**
    * A simple thread task representation
    * @author Pierre-Hugues Charbonneau
    *
    */

    public class Task {

    // Object used for FLAT lock
    private final Object sharedObject = new Object();
    // ReentrantReadWriteLock used for WRITE & READ locks
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    /**
    * Execution pattern #1
    */

    public void executeTask1() {

    // 1. Attempt to acquire a ReentrantReadWriteLock READ lock
    lock.readLock().lock();

    // Wait 2 seconds to simulate some work...
    try { Thread.sleep(2000);}catch (Throwable any) {}

    try {
    // 2. Attempt to acquire a Flat lock...
    synchronized (sharedObject) {}
    }
    // Remove the READ lock
    finally {
    lock.readLock().unlock();
    }
    System.out.println("executeTask1() :: Work Done!");
    }

    /**
    * Execution pattern #2
    */

    public void executeTask2() {
    // 1. Attempt to acquire a Flat lock
    synchronized (sharedObject) {

    // Wait 2 seconds to simulate some work...
    try { Thread.sleep(2000);}catch (Throwable any) {}

    // 2. Attempt to acquire a WRITE lock
    lock.writeLock().lock();

    try {
    // Do nothing
    }

    // Remove the WRITE lock
    finally {
    lock.writeLock().unlock();
    }
    }
    System.out.println("executeTask2() :: Work Done!");
    }

    public ReentrantReadWriteLock getReentrantReadWriteLock() {
    return lock;
    }
    }

    该实例的详细分析