线程间通信之wait/notify

前言


wait和notify这两个方法通常在线程间通信中常常用到,其中wait方法和sleep方法虽然都能够让线程“停下来”,但是有很大的区别,另外,耳熟能详的生产者消费者问题也涉及到wait和notify,本篇就简单的整理一下这些问题吧!

一、wait()/notify()的简单使用

用一个例子来看一下简单的使用:假设有两个线程,一个线程用来判断数组中长度是否达到5,达到的时候通知一下,另一个线程用来往数组中添加元素。

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
public class demo1 {
static List<Integer> list = new ArrayList<>();
static Object lock = new Object();

static class Thread1 extends Thread{
@Override
public void run() {
synchronized (lock) {
if (list.size() != 5) {
System.out.println("start..." + Thread.currentThread().getName());
try {
lock.wait();
System.out.println("end..." + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

}
}

static class Thread2 extends Thread{
@Override
public void run() {
synchronized (lock) {
System.out.println("start..." + Thread.currentThread().getName());
for (int i = 0 ; i < 10 ; ++i) {
list.add(i);
System.out.println("当前list的数量 " + list.size());
if (list.size() == 5) {
lock.notify();
System.out.println("释放锁,但是当前线程还需要继续执行下去...");
}
}
System.out.println("end..." + Thread.currentThread().getName());
}
}
}

public static void main(String[] args) throws InterruptedException {
Thread1 thread1 = new Thread1();
Thread2 thread2 = new Thread2();
thread1.start();
Thread.sleep(20);
thread2.start();
}
}

执行结果

start…Thread-0
start…Thread-1
当前list的数量 1
当前list的数量 2
当前list的数量 3
当前list的数量 4
当前list的数量 5
释放锁,但是当前线程还需要继续执行下去…
当前list的数量 6
当前list的数量 7
当前list的数量 8
当前list的数量 9
当前list的数量 10
end…Thread-1
end…Thread-0

从结果可以看出,Thread1是先抢占到锁的,但是由于执行了wait(),立即释放对锁的控制,Thread2抢占到锁,Thread2立即执行,当长度等于5的时候,执行了notify()命令,唤醒Thread1,但不是立即释放掉锁,而是要等Thread2执行完毕后才释放掉锁,所以数组元素一直添加到10后,Thread2释放掉锁,然后Thread1继续执行wait()后面的操作。

总结起来是这样的,wait()命令会直接释放掉锁,但是notify()要等到执行完notify()方法所在的同步synchronized代码块后才释放锁,注意,这边是执行完synchronized代码块!!!

既然wait()是暂停了线程,那么和sleep()有什么区别?其实wait()是立即释放掉锁,还让出CPU的执行权,进入等待,但是sleep()是不释放锁,相当于将当前线程进入阻塞状态(BLOCKED),仅仅会让出CPU的执行权。这里最经典的一个问题就是生产者消费者问题。

二、生产者消费者问题

简单来说,生产者消费者问题是指在一个系统中,有生产者和消费者两种角色,生产者生产消费者需要的资料,消费者消费生产出来的资料,当生产者生产的资料不够的时候,消费者等待,当生产者生产出资料后,通知消费者来拿资料。

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
package test.waitAndnotify;

import java.util.LinkedList;
import java.util.Queue;

/**
* @Author: hqf
* @description:
* @Data: Create in 10:53 2019/10/21
* @Modified By:
*/
public class ProducerConsumer {
private static final int CAPACITY = 5;

/**
* 生产者
*/
static class Producer extends Thread{
private Queue<Integer> queue;
int maxSize;
int i = 0;

public Producer(Queue<Integer> queue, int maxSize) {
this.queue = queue;
this.maxSize = maxSize;
}

@Override
public void run() {
while (true) {
synchronized (queue) {
while (queue.size() == maxSize) {
try {
System.out.println(Thread.currentThread().getName() + " 生产者生产数量已满,等待消费者线程消费");
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " 生产出" + i);
queue.add(i);
i++;
queue.notifyAll(); //通知所有消费者可以消费,或者其他生产者可以继续生产
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

/**
* 消费者
*/
static class Consumer extends Thread{
private Queue<Integer> queue;
int maxSize;

public Consumer(Queue<Integer> queue, int maxSize) {
this.queue = queue;
this.maxSize = maxSize;
}

@Override
public void run() {
while (true) {
synchronized (queue) {
while (queue.isEmpty()) {
System.out.println(Thread.currentThread().getName() + " 当前生产队列为空,消费者等待生产者生产");
try {
queue.wait();
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int x = queue.poll();
System.out.println("消费者消费 " + x);
queue.notifyAll(); //通知所有线程可以工作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<>();
Producer producer1 = new Producer(queue, CAPACITY);
Consumer consumer1 = new Consumer(queue, CAPACITY);
producer1.start();
consumer1.start();
}
}

代码的执行结果

Thread-0 生产出0
Thread-0 生产出1
Thread-0 生产出2
Thread-0 生产出3
消费者消费 0
Thread-0 生产出4
Thread-0 生产出5
Thread-0 生产者生产数量已满,等待消费者线程消费
消费者消费 1
Thread-0 生产出6
Thread-0 生产者生产数量已满,等待消费者线程消费
消费者消费 2
消费者消费 3
消费者消费 4
消费者消费 5
消费者消费 6
Thread-0 生产出7
Thread-0 生产出8
Thread-0 生产出9

…………

同样的,也能设置多个生产者消费者

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<>();
Producer producer1 = new Producer(queue, CAPACITY);
Producer producer2 = new Producer(queue, CAPACITY);
Consumer consumer1 = new Consumer(queue, CAPACITY);
Consumer consumer2 = new Consumer(queue, CAPACITY);
Consumer consumer3 = new Consumer(queue, CAPACITY);
producer1.start();
producer2.start();
consumer1.start();
consumer2.start();
consumer3.start();
}

代码的执行结果

Thread-0 生产出0
Thread-0 生产出1
Thread-0 生产出2
Thread-0 生产出3
Thread-4 消费者消费 0
Thread-4 消费者消费 1
Thread-4 消费者消费 2
Thread-4 消费者消费 3
Thread-4 当前生产队列为空,消费者等待生产者生产
Thread-3 当前生产队列为空,消费者等待生产者生产
Thread-2 当前生产队列为空,消费者等待生产者生产
Thread-1 生产出0
Thread-1 生产出1
Thread-1 生产出2
Thread-1 生产出3
Thread-1 生产出4
Thread-1 生产者生产数量已满,等待消费者线程消费
Thread-2 消费者消费 0
Thread-2 消费者消费 1
Thread-3 消费者消费 2
Thread-3 消费者消费 3
Thread-3 消费者消费 4
Thread-3 当前生产队列为空,消费者等待生产者生产
Thread-4 当前生产队列为空,消费者等待生产者生产
Thread-0 生产出4
Thread-0 生产出5

……

但是要注意了,这边同步代码块synchronized中的条件要是while()而不能是if(),这是为什么?我们来将while()改成if()试试,结果如下:

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
Thread-0 生产出0
Thread-0 生产出1
Thread-0 生产出2
Thread-0 生产出3
Thread-0 生产出4
Thread-0 生产者生产数量已满,等待消费者线程消费
Thread-4 消费者消费 0
Thread-4 消费者消费 1
Thread-4 消费者消费 2
Thread-4 消费者消费 3
Thread-4 消费者消费 4
Thread-4 当前生产队列为空,消费者等待生产者生产
Thread-3 当前生产队列为空,消费者等待生产者生产
Thread-2 当前生产队列为空,消费者等待生产者生产
Thread-1 生产出0
Thread-1 生产出1
Thread-2 消费者消费 0
Thread-2 消费者消费 1
Exception in thread "Thread-3" java.lang.NullPointerException
at test.waitAndnotify.ProducerConsumer$Consumer.run(ProducerConsumer.java:79)
Thread-0 生产出5
Exception in thread "Thread-4" java.lang.NullPointerException
at test.waitAndnotify.ProducerConsumer$Consumer.run(ProducerConsumer.java:79)
Thread-0 生产出6
Thread-0 生产出7

很明显,发生了错误。来分析一下,比如说当前执行到一个生产者生产出了一个数字,此时队列中恰好只有两个数字,接着唤醒了其他的线程,所以三个消费者线程都被唤醒,要进行消费,但是当消费者线程1和消费者线程2都消费完后,消费者线程3要进行消费了,由于他们都是从wait()后面开始执行的,消费者线程1和2是没有问题的,因为队列中有两个数字可供消费,但是当消费者线程3开始执行后,由于是从wait()后面开始执行的,就直接跳出if()判断,进行消费,而此时队列中没有其他数字来供他消费,所以报错。也就是上面报错的原因。

但是,当改成while()循环的时候,线程3会回到while()这里进行再判断,当队列中没有元素,就继续while()循环,而不是执行下去,这样就不会发生错误。