0%

java | 设计模式 同步模式-保护性暂停 解耦多模式

这是对

的再次升级分析。

之前虽然可以进行线程间的唤醒,但是,都是一个线程借助一个锁,唤醒另外一个线程。

但是,我们想多个线程借助一个模块,唤醒多个对应线程。

设计一个用来解耦的中间类,不仅能够解耦「结果等待者」和「结果生产者」,同时还能够同时支持多个任务的管理。

考虑以下的场景

  • 有一个相亲晚会,相亲晚会的一个节目是,女生收到男生的情书
  • 每个女生只能收到一个情书
  • 男生只能有一个心仪的女生,并且写情书,为了公平,男生选择好一个女生后,其他男生就不能选择该女生了
  • 在规定时间内,男生把情书写好,放到信箱中,等待心仪女生获取

上面只是一个小场景,不必特别在意「其实是为了代码逻辑,硬想出的场景」,但是,有一个限制点需要特别提示一下

  • 女生和男生之间是点对点连接
  • 信箱只是暂时存放信件

代码如下

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
package com.redisc;

import lombok.extern.slf4j.Slf4j;

import java.util.Hashtable;
import java.util.Map;
import java.util.Set;

@Slf4j(topic = "c.Run")
public class Run {

public static void main(String[] args) throws Exception {
for (int i = 0; i < 3; i++) {
new girl().start();
}
Thread.sleep(1000);

for (Integer id : Mailboxes.getIds()) {
new boy(id, "情书内容:做我女朋友吧" + id).start();
}
}

}

@Slf4j(topic = "c.Girl")
class girl extends Thread {
@Override
public void run() {
// 收信
GuardedObject guardedObject = Mailboxes.createGuardedObject();
log.debug("等待情书 女生 id {}", guardedObject.getId());
Object mail = guardedObject.get(5000);
log.debug("收到情书 女生 id:{} 内容:{}", guardedObject.getId(), mail);
}
}

@Slf4j(topic = "c.Boy")
class boy extends Thread {
private int id;
private String mail;

public boy(int id, String mail) {
this.id = id;
this.mail = mail;
}

@Override
public void run() {
GuardedObject guardedObject = Mailboxes.getGuardedObject(id);
log.debug("写情书并发送 id:{} 内容 {}", id, mail);
guardedObject.complete(mail);
}
}

class Mailboxes {
// 由于是多线程访问,所以,要注意线程安全问题
private static Map<Integer, GuardedObject> boxes = new Hashtable<>();

private static int id = 1;

// 产生唯一 id
private static synchronized int generateId() {
return id++;
}

public static GuardedObject getGuardedObject(int id) {
return boxes.remove(id); // 一次性,要移除
}

public static GuardedObject createGuardedObject() {
GuardedObject go = new GuardedObject(generateId());
boxes.put(go.getId(), go);
return go;
}

public static Set<Integer> getIds() {
// 为什么不需要加 synchronized,是因为 Hashtable 本来就是线程安全的
return boxes.keySet();
}
}

class GuardedObject {
private int id;
private Object response;

public GuardedObject(int id) {
this.id = id;
}

public int getId() {
return id;
}

// 获取结果
public Object get(long timeout) {
// 开始时间
long begin = System.currentTimeMillis();
synchronized (this) {
// 经历时间
long passedTime = 0;
// 解决虚假唤醒
while (response == null) {
if (passedTime >= timeout) {
break;
}
try {
this.wait(timeout - passedTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 求得经历时间
passedTime = System.currentTimeMillis() - begin;
}
return response;
}
}

// 产生结果
public void complete(Object response) {
synchronized (this) {
// 给结果成员变量赋值
this.response = response;
this.notifyAll();
}
}
}

输出

1
2
3
4
5
6
7
8
9
14:30:16.034 [Thread-2] DEBUG c.Girl - 等待情书 女生 id 2
14:30:16.035 [Thread-1] DEBUG c.Girl - 等待情书 女生 id 3
14:30:16.034 [Thread-0] DEBUG c.Girl - 等待情书 女生 id 1
14:30:17.048 [Thread-4] DEBUG c.Boy - 写情书并发送 id:2 内容 情书内容:做我女朋友吧2
14:30:17.048 [Thread-3] DEBUG c.Boy - 写情书并发送 id:3 内容 情书内容:做我女朋友吧3
14:30:17.049 [Thread-5] DEBUG c.Boy - 写情书并发送 id:1 内容 情书内容:做我女朋友吧1
14:30:17.049 [Thread-1] DEBUG c.Girl - 收到情书 女生 id:3 内容:情书内容:做我女朋友吧3
14:30:17.049 [Thread-2] DEBUG c.Girl - 收到情书 女生 id:2 内容:情书内容:做我女朋友吧2
14:30:17.049 [Thread-0] DEBUG c.Girl - 收到情书 女生 id:1 内容:情书内容:做我女朋友吧1

根据

  • Mailboxes 是图中的 Futures
  • MailBoxes 暂时存放中间信息传递

这时候有的人就问,必须要 MailBoxes 吗?答案是当然可以,看下面的代码

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
package com.redisc;

import lombok.extern.slf4j.Slf4j;

import java.util.*;


@Slf4j(topic = "c.Run")
public class Run {

public static void main(String[] args) throws Exception {
// 这里其实拿到信件后,要删除,要是没删除,后续可能会撑爆内存
List<girl> girls = new ArrayList<>();
for (int i = 0; i < 3; i++) {
girl girl_ = new girl();
girls.add(girl_);
girl_.start();
}
Thread.sleep(1000);

girls.forEach(
(g) -> {
new boy(g, "做我女朋友").start();
}
);
}

}

@Slf4j(topic = "c.Girl")
class girl extends Thread {

private GuardedObject guardedObject = new GuardedObject();

public GuardedObject getGuardedObject() {
return guardedObject;
}

@Override
public void run() {
// 收信
log.debug("等待情书 女生");
Object mail = guardedObject.get(5000);
log.debug("收到情书 女生 内容:{}", mail);
}
}

@Slf4j(topic = "c.Boy")
class boy extends Thread {
private String mail;
private girl girl_;

public boy(girl girl_, String mail) {
this.girl_ = girl_;
this.mail = mail;
}

@Override
public void run() {
log.debug("写情书并发送 内容 {}", mail);
this.girl_.getGuardedObject().complete(mail);
}
}

class GuardedObject {
private Object response;

// 获取结果
public Object get(long timeout) {
// 开始时间
long begin = System.currentTimeMillis();
synchronized (this) {
// 经历时间
long passedTime = 0;
// 解决虚假唤醒
while (response == null) {
if (passedTime >= timeout) {
break;
}
try {
this.wait(timeout - passedTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 求得经历时间
passedTime = System.currentTimeMillis() - begin;
}
return response;
}
}

// 产生结果
public void complete(Object response) {
synchronized (this) {
// 给结果成员变量赋值
this.response = response;
this.notifyAll();
}
}
}

功能当然可以实现,但是,耦合性非常高,不利于以后的功能替换。

请我喝杯咖啡吧~