线程的通信
# 前言
线程的通信是比同步简单很多,说白了就是调用几个方法,在一些业务场景中我们需要用到线程的通信。
# wait()
与notify()
和notifyAll()
- wait():一旦执行此方法,当前线程就进入阻塞状态,并且释放同步监视器。
- notify():一旦执行此方法,就会唤醒一个正在
wait
的一个线程。如果有多个线程wait
,就唤醒优先级高的。 - notifyAll():一旦执行此方法,就会唤醒全部正在
wait
的线程。
这三个方法的调用者必须是同步代码块或同步方法中的同步监视器,如果你用this作为同步监视器,那么就必须用this来调用者三个方法。
因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized
的同步锁,因此这三个方法只能在Object
类中声明。
# 例题
使用两个线程打印1-100。线程1, 线程2 交替打印。
未加入线程通信
class Number implements Runnable {
private int num = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
if (num <= 100) {
System.out.println(Thread.currentThread().getName() + ":" + num);
num++;
} else {
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);java
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
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
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
可以看到输出结果基本上都是线程1,根本实现不了我们想要的效果。接下来改进:
class Number implements Runnable {
private int num = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
//唤醒一个线程
notify();
if (num <= 100) {
System.out.println(Thread.currentThread().getName() + ":" + num);
num++;
try {
//使得当前线程进入阻塞状态,并释放锁
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
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
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
这样就能解决问题了,可以交替输出了。
当线程1输出后,就进入阻塞状态,并且释放锁。
线程2获取同步锁进入代码块,唤醒线程1,线程 1无同步锁,只能就绪等待,线程2继续执行代码,然后进入阻塞状态,释放同步锁。
线程1这时就能获取到同步锁,唤醒线程2,线程2无同步锁,就能就绪等待,线程1继续执行代码,然后进入阻塞状态,释放同步锁
……
就这样就能交替输出奇数和偶数直到100为止。
# slee()和wait()的异同?
相同点:都可以使得当前的线程进入阻塞状态。
不同点:
- 声明位置不同:
sleep()
声明在Thread
类中,wait()
声明在Object
类中。 - 调用的要求不同:
sleep()
可以在任何需要的场景下调用,wait()
必须使用在同步代码块或同步方法中。 - 关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,
sleep()
不会释放锁,wait()
会释放锁。
# 经典例题
生产者/消费者问题:
- 生产者(Producer)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),
- 如果生产者试图生产更多的产品,店员会叫生产者停一下,
- 如果店中有空位放产品了再通知生产者继续生产;
- 如果店中没有产品了,店员会告诉消费者等一下,
- 如果店中有产品了再通知消费者来取走产品。
这里可能出现两个问题:
- 生产者比消费者快时,消费者会漏掉一些数据没有取到。
- 消费者比生产者快时,消费者会取相同的数据。
分析
1、是否多线程问题?是,生产者线程,消费者线程。
2、是否有共享数据?是,店员(或产品、产品的数量)
3、如何解决线程的安全问题?同步机制,有三种方法
4、是否设计线程通信?是
package com.saul.example;
/**
* 线程通信的应用:生产者/消费者问题
* 生产者(Producer)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),
* 如果生产者试图生产更多的产品,店员会叫生产者停一下,
* 如果店中有空位放产品了再通知生产者继续生产;
* 如果店中没有产品了,店员会告诉消费者等一下,
* 如果店中有产品了再通知消费者来取走产品。
* <p>
* 分析:
* 1、是否多线程问题?是,生产者线程,消费者线程。
* 2、是否有共享数据?是,店员(或产品、产品的数量)
* 3、如何解决线程的安全问题?同步机制,有三种方法
* 4、是否设计线程通信?是
*
* @author Saul.J.Wu
* @date 2020/12/21
*/
/**
* 店员
*/
class Clerk {
private int stock = 0;
private static int number = 0;
/**
* 接收生产者的商品
*/
public synchronized void acceptGoods() {
//店员一次只能持有固定数量的产品(比如:20)
if (stock < 20) {
number++;
stock++;
System.out.println(Thread.currentThread().getName()+":开始生产第"+number+"个商品");
//如果店中有产品了再通知消费者来取走产品。
notify();
}else{
//如果生产者试图生产更多的产品,店员会叫生产者停一下,
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 售出商品给消费者
*/
public synchronized void sellgoods() {
if (stock > 0) {
System.out.println(Thread.currentThread().getName() + ":开始消费第" + number + "个商品");
System.out.println();
stock--;
//如果店中有空位放产品了再通知生产者继续生产;
notify();
} else {
//如果店中没有产品了,店员会告诉消费者等一下,
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 生产者
*/
class Producer extends Thread {
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println("生产者: " + this.getName() + "开始生产商品……");
while (true) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
//生产者(Producer)将产品交给店员(Clerk)
clerk.acceptGoods();
}
}
}
/**
* 消费者
*/
class Customer extends Thread {
private Clerk clerk;
public Customer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println("消费者: " + this.getName() + "开始取走商品……");
while (true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//消费者(Customer)从店员处取走产品
clerk.sellgoods();
}
}
}
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk);
p1.setName("生产者1");
p1.start();
Customer c1 = new Customer(clerk);
c1.setName("消费者1");
c1.start();
Customer c2 = new Customer(clerk);
c2.setName("消费者2");
c2.start();
}
}
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
执行main
方法:
帮我改善此页面 (opens new window)
上次更新: 2020/12/21, 04:53:25