【多线程:设计模式】保护性暂停的应用与扩展
01.join实现原理
join源码
我们来分析一下它的源码
我们可以看出如果join给定的时间小于0就抛出异常如果给定的时间为0,其实也就是不加参数的join,他就会判断是否isAlive也就是判断调用join的线程是否存活,如果存活则wait(0)即一直等待,注意这里的wait是让t1.join同步的线程等待 t1线程运行。举一个例子就是在main线程运行t1.join(),那么isAlive判断的是t1线程是否存在,wait(0)是让main线程进行等待
如果给定的时间大于0,那么这里的代码就和我们的上一篇内容里的保护性暂停扩展-增加超时 一模一样了,这里就是 保护性暂停 这个设计模式的体现
join方法整体就是 保护性暂停 设计模式 的体现,只不过它没有传递数据 只是用来让其他线程陷入等待 使其调用join的线程可以先执行
02.扩展-解耦等待与成产
介绍
这个图中 t0 t2 t4是寄信人线程,t1 t3 t5负责邮递员线程 负责送信与代写信。每个信都有自己的id与内容 id可以理解为收信人的地址,寄信人通过邮递员把新送给收信人,这里我们只研究寄信人与邮递员之间的消息传递。注意:一个邮递员只能寄一封信。
为什么要这样设计
这样设计的好处是解耦 也就是降低消息传递的复杂度与关联度,我们把Futures当做邮局 寄信人 把信给邮局 然后邮递员来取信送出,我们可以想象一下 如果没有这个邮局 我们会怎么样?我们需要亲自跑到邮递员家里 把信给他 这期间邮递员可能有事出门了 你就白去了 总之这样做 效率很低,这就是邮局的好处。
代码
@Slf4j(topic = "c.Test20")
public class Test20 {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new People().start();
}
Sleeper.sleep(1);
for (Integer id : Mailboxes.getIds()) {
new Postman(id, "内容" + id).start();
}
}
}
@Slf4j(topic = "c.People")
class People 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.Postman")
class Postman extends Thread {
private int id;
private String mail;
public Postman(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() {
return boxes.keySet();
}
}
// 增加超时效果
class GuardedObject {
// 标识 Guarded Object
private int id;
public GuardedObject(int id) {
this.id = id;
}
public int getId() {
return id;
}
// 结果
private Object response;
// 获取结果
// timeout 表示要等待多久 2000
public Object get(long timeout) {
synchronized (this) {
// 开始时间 15:00:00
long begin = System.currentTimeMillis();
// 经历的时间
long passedTime = 0;
while (response == null) {
// 这一轮循环应该等待的时间
long waitTime = timeout - passedTime;
// 经历的时间超过了最大等待时间时,退出循环
if (timeout - passedTime <= 0) {
break;
}
try {
this.wait(waitTime); // 虚假唤醒 15:00:01
} catch (InterruptedException e) {
e.printStackTrace();
}
// 求得经历时间
passedTime = System.currentTimeMillis() - begin; // 15:00:02 1s
}
return response;
}
}
// 产生结果
public void complete(Object response) {
synchronized (this) {
// 给结果成员变量赋值
this.response = response;
this.notifyAll();
}
}
}
结果
12:21:51.157 c.People [Thread-1] - 开始寄信 id:3
12:21:51.158 c.People [Thread-0] - 开始寄信 id:1
12:21:51.157 c.People [Thread-2] - 开始寄信 id:2
12:21:52.171 c.Postman [Thread-4] - 送信 id:2, 内容:内容2
12:21:52.171 c.Postman [Thread-5] - 送信 id:1, 内容:内容1
12:21:52.171 c.Postman [Thread-3] - 送信 id:3, 内容:内容3
12:21:52.171 c.People [Thread-0] - 寄信的内容 id:1, 内容:内容1
12:21:52.171 c.People [Thread-2] - 寄信的内容 id:2, 内容:内容2
12:21:52.171 c.People [Thread-1] - 寄信的内容 id:3, 内容:内容3
对上述结果进行分析与解释
解释
我们可以看出 我们寄信后每个信 都有一个对应的 邮递员来送信 信的内容与id也一致
分析
我们首先关注到了几个类分别是:
GuardedObject
GuardedObject 类是写入消息与获取消息的类,由于信是邮递员代写 所以邮递员写信就是写入消息 收信人得到信就是获取消息
这个类有几个方法:
getId:获取这个信的id
get:获取这个信,有时限的等待,如果获取时间过长就放弃获取
complete:写信
Mailboxes
Mailboxes类 可以理解为邮局 负责解耦寄信人与邮递员,主要形式是通过id与信之间的一一对应实现,邮递员不需要知道寄信的是谁 他只需要关注他要寄到的地址(就是信的id)是什么。
这个类有几个方法与成员变量:
boxes变量:是一个Hashtable类型 因为Hashtable是线程安全的 所以不用考虑关于 用到有关Hashtable的方法的线程安全问题,Hashtable的key是id value是信,我们通过id找到信
generateId方法:作用是生产信的id,在createGuardedObject创建信时被调用
getGuardedObject方法:作用是邮递员通过id获取信 然后邮递员送信 因为已经送信了 所以我们需要把它的Hashtable删除,所以我们这里选择使用的方法是boxes.remove(id); 通过remove获取信 并且删除它的Hashtable
createGuardedObject方法:作用是创建一封信 也就是一个Hashtable,可以理解为寄信人 要寄信 需要先创建一个信封并且在信封上写上地址(id)
getIds方法:这个方法的作用是获取所有id,目的是 让邮递员选择一个id对应的信送出
Postman
Postman线程类 是邮递员类,作用是送信和 代写信
这个类有几个方法与成员变量:
id变量:要送信的id
mail变量:要送信的内容
Postman:有参构造,参数为id mail,通过id获取到信(内容为空) 然后往其中写入内容mail
run方法:作用为每个线程 都在run方法 中 通过id获取信 并且写入内容mail
People
People线程类 是寄信人类,作用是 创建信
这个类的方法:
run方法:作用是创建信,并 在一段时间后 获取信的内容,如果5s后仍然没有获取到则放弃获取 说明信的内容写入超时
Test20
Test20类是入口类
创建了3个People线程与3个Postman线程,并且在People线程运行1s后 Postman线程开始运行,模拟1s后送信到邮局