您的位置:首页 > 编程语言

【编程素质】多线程编程之设计模式

2017-01-31 01:03 274 查看
全文是读书笔记。书为:《Java多线程编程实战指南 设计模式篇》黄海文 著

1,可复用实现代码

(1)遍历线程安全的集合时加锁

多线程环境中,遍历一个集合对象时,即便被遍历的对象本身是线程安全的,为防止遍历过程中该集合的内部结构(增删)被其他线程改变而导致出错,也引入锁。

Vector vector = null;

//此处以vector本身为锁,防止遍历过程中的其他线程改变其内部结构
synchronized(vector){
for(int i =0; i<vector.size(); i++){
doSomethingWith(vector.get(i));
}
}


(2)GOF设计模式



复合模式:MVC和三层架构

设计模式-单例模式

设计模式-模板方法

设计模式-观察者模式

设计模式-装饰模式:处于安全目的,保护被访问者

设计模式-策略模式

设计模式-工厂模式:工厂方法、简单工厂、抽象工厂

设计模式-命令模式

设计模式-适配器模式(Adapter)

设计模式-外观模式

设计模式-桥接模式

设计模式-代理模式(Proxy Pattern)、委托模式:动态扩充对象功能

迭代器模式

组合模式

状态模式

生成器模式Builder Pattern

责任链模式

蝇量模式Flyweight Pattern

解释器模式Interpreter Pattern

中介者模式Mediator Pattern

备忘录模式MementoPattern

原型模式Prototype Pattern

访问者模式Visitor Pattern

控制反转(IoC)与依赖注入(DI)

2,Immutable Object(不可变类)模式

(1)相关概念了解

①保护性拷贝(defensive copy)

/**
*这是反例,没有用保护性拷贝,造成数据被修改。
*原因:Date类是一个可变类。
*/
public class Person {

private String name;
private Date birth;

public Person(String name, Date birth) {
this.name = name;
this.birth = birth;
}

public Date getBirth() {
return birth;
}

public String getName() {
return name;
}
}
public static void main(String[] args) {

Person p = new Person("Benson",new Date(1990, 4, 13));
System.out.println(p.getName());
System.out.println(p.getBirth().getYear());

Date hole = p.getBirth();
hole.setYear(2013);

System.out.println(p.getName());
System.out.println(p.getBirth().getYear());
}


进行保护性拷贝,应该写成:

public class Person {

...
public Date getBirth() {
/**
*保护性拷贝
*其它不变,只修改这一句即可实现保护性拷贝。
*实现原理:每次获取的是新对象,原来的对象不会被修改(只要传进来,传出去的值中途不会被修改)。
*/
return new Date(birth.toString());
}
...
}


只要能够不创建对象,就不要创建多余的对象。(为了尽可能减少资源的占用,提高运行效率)

只要需要创建对象,就不要吝啬地创建它(为了安全性)。–>保护性拷贝。

②不可变类

不对外暴露任何可以修改其参数的方法。

i>类本身用final修饰,防止被修改。

ii>所有字段用final修饰。

iii>对象创建过程中,不对外泄露this。

iv>引用其他可变对象(如数组、集合),则设为私有且不对外暴露。如果有相关方法要返回这些值,进行保护性拷贝(Defensive Copy)。

(2)场景

保证在不使用显式锁的情况下保证线程安全。

将现实世界中状态可变的实体建模为状态不可变的对象,通过创建不同状态不可变的对象来反应其状态变更。

①建模对象状态变化不频繁。

可以设置一个线程用于状态变化时创建不可变对象,其他线程则只是读取不可变对象。如果变化频繁,会频繁创建新的不可变对象,这回增加JVM GC的负担和CPU消耗,需综合考虑。

②一组数据进行操作需要保证原子性。

将这组数据组合成不可变对象进行建模。

③某个对象作为安全的HashMap的key

一个对象状态发生变化,其HashCode也变化,导致同样的对象作为key去 get HashMap的时候无法获取关联的值。

(3)实现



(4)举例

JDK 1.5中引入的类java.util.concurrent.CopyOnWriteArrayList应用了Immutable Object模式。

它适用遍历操作频率比添加和删除操作更加频繁的场景。

(5)相关模式

①Thread Specific Storage(线程特有存储)模式。—>3

②Serial Thread Confinement模式。—>4

用一种更小的锁开销去(队列所涉及 的锁开销)去替代另外一种可能更大的锁开销(工作者线程如果采用锁所带来的开销)。

3,Thread Specific Storage(线程特有存储)模式

(1)概念

①JDK 1.2的Java.lang.ThreadLocal

Thread Specific Storage的一种应用。

根据在线文档-jdk-zh,ThreadLocal提供了thread-local变量。

ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。



/*
* 查看在线文档可知,initialValue方法是protected,显然是为了让子类覆盖它。
* 通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值。
*/
private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {
//此处0即为下文的TSObject对象。
public Integer initialValue() {
return 0;
}
};


②java.security.SecureRandom强随机

与java.math.Random这种伪随机数生成器不同,其生成结果非确定性,可使用特定的随机数生成算法。

详细参考在线文档-jdk-zh

(2)场景

①各个线程访问各自的变量,相互间不共享变量,从而实现了线程安全,避免了锁的消耗以及相关问题(死锁、上下文切换)。

②缺点

隐藏了各个对象间关系,可读性变差。
在线程池环境下使用可能导致数据错乱,所以在特定任务使用后,要记得remove掉新对象。

③对比传统的synchronize共享变量

synchronize是通过锁机制进行时间换空间,而Thread Specific Storage是存储拷贝进行空间换时间

(3)实现

①线程特有对象:TSObject(Thread Specific Object)

这是多个线程都要访问的类(根据实际情况规定 类型,如TSObject为Integer)。

它会被每一个线程持有,每个线程对其状态的修改不会影响到其他线程。

②线程特有对象代理类:TSObjectProxy

不同线程通过统一接入点(TSObjectProxy)获取该线程所特有的TSObject实例。(相当于全局变量)

-getThreadSpecific:获取TSObjectProxy实例关联的TSObject.

-setThreadSpecific:建立关联。

-removeThreadSpecific:删除关联。

③线程特有存储(Thread Specific Storage)

相当于Map,每个线程都有一个Map。Map中元素的值(value)为一个TSObject实例,键(Key)为TSObjectProxy实例(即存储线程对象)。

(4)demo

/**
*系统功能分析:支持验证码短信功能。
*①用户进行一些重要操作时,系统随机(java.security.SecureRandom强随机)生成一个验证码,发给用户。
*②SecureRandom初始化耗时,所以每次需要时复用SecureRandom更好。
*③用于生成随机数的nextInt方法最终会调用一个由SecureRandom自定义的synchronized方法,一个线程共同调用该实例的这个方法时,其他线程只能等待。所以采用Thread Specific Storage方法更好:让每个需要生成验证码的线程生成一个且仅有一个SecureRandom实例。
*/
public class SmsVerficationCodeSender{

/**
* 创建了线程池
* java.util.concurrent.ExecutorService
*/
private static final ExecutorService es = new ThreadPoolExecutor(1, Runtime.getRuntime().availableProcessors(), 60, new ThreadFactory(){

@Override
public Thread newThread(Runnable r){
Thread t = new Thread(r, "VerfCodeSender");
t.setDaemon(true);
return t;
}
}, new ThreadPoolExecutor.DiscardPolicy());

public static void main(String[] args){
SmsVerficationCodeSender client = new SmsVerficationCodeSender();
client.sendVerificationSms("13911111111");
client.sendVerificationSms("15099999999");

try{
Thread.sleep(100);
}catch(Exception e){

}
}

/*
* msisdn 电话号码
*/
public void sendVerificationSms(final String msisdn){

Runnable task = new Runnable(){

@Override
public void run(){

//生成强随机数验证码
int verificationCode = ThreadSpecificSecureRandom.instance.nextInt(999999);
//字符串格式
DecimalFormat df = new DecimalFormat("000000");
String txtVerCode = df.format(verificationCode);
//发送验证码短信
sendSms(msisdn, txtVerCode);
}
};
/*
*在Java5以后,通过Executor来启动线程比用Thread的start()更好.
*将Runnable的对象传递给ExecutorService的submit方法,则该run方法自动在一个线程上执行。
*/
es.submit(task);
}

private void sendSms(String msisdn, String verficationCode){
System.out.println("send verficationCode:" + verficationCode + "to " + msisdn);
}
}

/**
*类作用:这个单例类被多个需要生成验证码的线程共享,但这些线程调用nextInt方法时所用的SecureRandom实例是每个线程各自持有的线程特有对象TSObject。
*/
public class ThreadSpecificSecureRandom{

//单例
public static final ThreadSpecificSecureRandom instance = new ThreadSpecificSecureRandom();
private ThreadSpecificSecureRandom(){
}

/*
*Thread Specific Storage方法:
*TSObject--SecureRandom
*TSObjectProxy--SECURE_RANDOM
*Thread Specific Storage--使用java.lang.ThreadLocal
*/
private static final ThreadLocal<SecureRandom> SECURE_RANDOM = new ThreadLocal<SecureRandom>(){
//建立SecureRandom对象
@Override
protected SecureRandom initialValue(){
SecureRandom sr;
try{
//SHA1PRNG为一种随机数生成算法。SecureRandom可生成强随机数。
sr = SecureRandom.getInstance("SHA1PRNG");
}catch(Exception e){
//SecureRandom实例可以通过无参构造函数或getInstance(特定算法)获得
sr = new SecureRandom();
}
}
}

/**
*upperBound上界
*/
public int nextInt(int upperBound){
//获得当前SecureRandom实例。
SecureRandom sr = SECURE_RANDOM.get();
/*
*SecureRandom中的nextInt方法生成一个包含用户指定伪随机位数的整数。
*其中参数upperBound表示要生成的伪随机位数范围为0~upperBound
*/
return sr.nextInt(upperBound);
}

public void setSeed(long seed){
SecureRandom sr = SECURE_RANDOM.get();
//重新设置此随机对象的种子
sr.setSeed(seed);
}
}


(5)举例

该方案另一种小的实现是:

将线程需要访问的变量作为线程对象的私有实例变量。通过每个线程的构造函数传参。

缺点:通用性不够。

(6)相关模式

①Immutable Object模式 –>2

②Proxy模式 –>GOF设计模式代理模式

Thread Specific Storage模式的TSObjectProxy参与者相当于Proxy模式的Proxy参与者。

4,Serial Thread Confinement(串行线程封闭)模式

(1)概念

①java.util.concurrent.BlockingQueue阻塞队列

支持两个附加操作的队列。这两个附加的操作是:

在队列为空时,获取元素的线程会等待队列变为非空。

a)当队列满时,存储元素的线程会等待队列可用。

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

(2)场景

并发任务的执行涉及某个非线程安全对象,使用一个开销更小的锁(串行化并发任务时所用队列涉及的锁),去替代另一个可能开销更大的锁(为保障并发任务所访问的非线程安全对象可能引入的锁)。

(3)实现

①队列(Queue):

将多个并发任务存入队列实现任务的串行化。(此处涉及锁)

–enqueue:并发任务加入队列。

–dequeue:并发任务移除队列。

可以建立一个单独的类,或者直接在WorkerThread类中创建队列。

private final BlockingQueue<String> workQueue;


②唯一的工作者线程(WorkerThread):

负责处理这个队列访问的非线程安全对象。

–submit:将Serializer发送来的任务接收并存入队列,实现并发任务的串行化。

–dispatch:用于执行串行化任务。

③非线程安全对象(NonThreadSafeObject):

由于只有一个线程访问它,故不用加锁,从而避免锁带来的问题。

④Serializer:

由客户端调用。

对外暴露服务接口,管理工作者线程的生命周期,将客户端的并发调用转换为相应的任务以实现服务串行化。

–init:初始化对外暴露的服务,创建并启动WorkerThread。

–Service:串行化客户端的并发调用。
调用WorkerThread的submit方法,提交一个任务。

–shutdown:停止对外暴露的服务,如WorkerThread停止。

(4)相关模式

①Immutable Object模式 –>2

②Promise模式 –>5

③Producer-Consumer模式 –>6

④Thread Specific Storage模式 –>3

5,Promise(承诺)模式

(1)概念

①异步编程模式

异步操作通常用于执行完成时间可能较长的任务,如打开大文件、连接远程计算机或查询数据库。异步操作在主应用程序线程以外的线程中执行。应用程序调用方法异步执行某个操作时,应用程序可在异步方法执行其任务时继续执行。

②同步编程模式

是一种请求响应模型,调用一个方法,等待其响应返回。

同步编程的方式更为简单,缺点是:假如是单线程那么一旦遇到阻塞调用,会造成整个线程阻塞,导致cpu无法得到有效利用,若是多线程则不可避免的要考虑互斥和同步问题,而互斥和同步带来复杂度也很大。

③java.util.concurrent.FutureTask

可取消的异步计算。利用开始和取消计算的方法、查询计算是否完成的方法和获取计算结果的方法,此类提供了对 Future 的基本实现。仅在计算完成时才能获取结果;如果计算尚未完成,则阻塞 get 方法。一旦计算完成,就不能再重新开始或取消计算。



(2)场景

是一种异步编程模式:

我们可以先开始一个任务的执行,并得到一个用于获取执行结果的凭据对象,而不必等待其执行完成就可以继续其他操作。等到我们需要该任务的执行结果时,在调用凭据对象的相关方法来获取。

(3)实现

①Promisor

–compute:客户端调用,创建并启动异步任务TaskExecutor,返回Promise实例。

②Promise

包装异步任务处理结果。

–getResult:获取异步任务执行结果。如果此时异步任务未执行完,则该方法阻塞(调用方法的进程也阻塞).

–setResult:设置异步任务执行结果。

–isDone:检测异步任务是否执行完毕。

//在JAVA中Promise参与者可以直接使用JDK1.5提供的类java.util.concurrent.FutureTask来实现。
//通过task.get()获取Result。
//task.isdone()相当于Promise的isdone方法
final FutureTask<Result> task = new FutureTask<Result>(callable);


③Result

异步任务执行结果,类型由应用决定。

④TaskExecutor异步任务

–run:执行异步任务所代表的计算。创建Result实例,并设置到Promise实例上。(可以由专门的线程或线程池来调用run方法)

(4)优点

避免了不必要的等待,增加了系统的并发性,减少了代码复杂度。

(5)举例

去小吃店,同时点了紫菜蛋花汤和生煎包,付款后,拿到手的是小票(凭据对象)。由于紫菜蛋花汤快,可以拿小票先兑换到。在吃紫菜蛋花汤时,当生煎包好了,继续拿小票兑换。

(7)相关模式

①Guarded Suspension模式–>7

Promise模式这种通过线程阻塞而进行的等待可以看作Guarded Suspension模式的一个实例。

②Active Object模式–>8

Active Object模式可以看成是包含了Promise模式的符合模式。其Proxy参与者相当于Promisor参与者,返回值相当于Promise参与者。

③Master-Slave模式–>9

Master-Slave模式的Slave参与者相当于Promisor参与者,返回处理结果需要使用Promise模式。

Master-Slave模式的subService方法的返回值是Promise参与者实例。

④Factory Method模式–> 工厂模式

Promisor参与者可以看成是Factory Method模式的一个例子,Promisor的异步方法可以看成一个工厂方法,返回值是一个Promise实例。

6,Producer-Consumer(生产者/消费者)模式

(1)概念

①生产者/消费者

生产者:数据的提供方。消费者:数据的加工方。

(2)场景

为了避免数据的生产者和数据的消费者处理速率不一致,在之间引入一个通道(Channel,用队列实现)对二者进行解耦:生产者往通道放数据,消费者从通道取数据。

(3)实现

生产者和消费者运行在不同的线程中。

①Producer生产者

运行在客户端线程。

–service:由客户端调用,生成相应产品,并调用Channel参与者实例,将产品put。

②Product产品

生产者生产的数据或任务,类型由程序决定。

③Channel通道

接口。生产者与消费者之间的缓冲区,用于产品的传递。有容量限制。

–put:产品存入通道。

–take:从通道取产品。

④BlockingQueueChannel阻塞队列

实现Channel接口。

–put:产品存入通道。当队满时,将当前线程挂起直到队列非满。

–task:从队列取出产品。当对空时,将当前线程挂起直到队列非空。

private final BlockingQueue<P> queue;


⑤Consumer消费者

负责对产品进行处理。运行在其专门的工作者线程中。工作者线程使用Two-phase Termination模式(–>10),以实现该线程优雅停止。

–dispatch:从通道取产品并处理(调用Channel的put方法)。

(4)优缺点

将生产者和消费者二者解耦,避免二者处理速率不同问题。

(5)常见问题

①通道积压问题

可以使用有界阻塞队列:ArrayBlockingQueue和带容量限制的LinkedBlockingQueue

也可以使用带流量控制的无界阻塞队列(即对同一时间可以有多少个生产者线程往通道中存储产品限制)。

②锁的竞争

多个消费者从通道获取产品,其实是多个消费者的工作线程共享同一个队列实例,当修改队列的头指针时所需要获得的锁而导致的竞争。

工作窃取算法可以减轻其他消费者线程的负担,其思想为:

一个通道对应多个队列,当一个消费者线程处理完一个队列的产品时,可以继续从其他消费者线程的队列中取出产品进行处理,这样就避免了消费者线程闲置,减轻了其他消费者线程的负担,也不会造成锁的竞争。

③线程的停止

借助Two-phase Termination模式来停止Producer参与者的工作者线程,当所有Producer参与者都停止后,再停止该服务涉及的Consumer参与者的工作者线程。

(6)举例

java.util.concurrent.ThreadPoolExecutor:

是Producer-Consumer模式的可复用实现。其内部维护的工作队列(Channel参与者),工作者线程(Consumer参与者),其客户端代码则相当于Producer参与者。

(7)相关模式

①Guarded Suspension模式–>7

Producer-Consumer模式中,队满 Producer线程会被阻塞直到队列不满;队空时,Consumer线程会被阻塞直到队列不空。这两种线程的阻塞与唤醒的实现可以借助Guarded Suspension模式实现。

②Thread Pool模式–>11

Thread Pool模式也可以看作是Producer-Consumer模式的一个实例。

7,Guarded Suspension(保护性暂挂)模式

(1)场景

多线程编程中,为了提高并发性,将一个任务分解为不同部分,交由不同线程执行。可能会出现一个线程等待另一个线程完成一定的操作,Guarded Suspension模式能解决等待问题。

(2)实现

如果某个线程执行特定操作之前需要满足条件,在条件未满足之前将该线程暂停挂起(WAITING状态),直到满足 条件才继续运行。

①GuardedObject

–guardedMethod:受保护方法。

由客户端调用。

创建GuardedAction实例guardedAction。

以guardedAction作为参数调用Blocker实例的callWithGuard方法。

–stateChannged:改变GuardedObject实例状态的方法。在保护条件满足时唤醒受保护方法的执行线程。

由客户端调用。

创建java.util.concurrent.Callable实例stateOperation。

stateOperation封装了改变GuardedObject状态所需的操作。

以stateOperation为参数,调用Bocker的singnalAfter方法。

②GuardedAction

抽象类或接口。

–call:执行目标动作并返回。

–getGuard:获取保护条件Predicate。

③ConcreteGuardedAction

实现GuardedAction接口。应用程序所实现的具体目标动作及其关联的保护条件。

④Predicate

抽象类或接口。

–evaluate:用于表示保护条件的方法。

⑤ConcretePredicate

实现Predicate接口。应用程序具体保护条件。

⑥Blocker

–callWithGuard:执行ConcreteGuardedAction实现的目标操作和暂挂当前线程。

调用guardedAction的getGuard方法获取Predicate。

循环判断保护条件是否成立,成立则退出循环。否则,将当前线程挂起,即使其他线程将该线程唤醒,该循环依然继续检测。

调用guardedAction的call方法,获取返回值actionReturnValue。

–signalAfter:执行参数所制定动作,唤醒暂挂线程中的一个。

调用stateOperation对象的call方法以改变GuardedObject实例的状态,记录返回值shouldSignalBlocker。

如果shouldSignalBlocker为true,调用java.util.concurrent.locks.Condition实例的signal方法来唤醒该Condition所暂挂的线程中的一个。

–signal:唤醒暂挂线程中的一个。

–broadcastAfter:执行其参数指定动作,唤醒所以暂挂线程。

–broadcast:唤醒所有暂挂线程。

⑦ConditionVarBlocker

基于java条件变量(java.util.concurrent.locks.Condition)实现的Blocker.

为了避免锁泄露,使用ReentrantLock锁的临界区代码这样写:

//获得锁
lock.lockInterruptibly();
try{
//临界区代码
}finally{
//在finally块中释放锁,保证锁总是会被释放的
lock.unlock();
}


(3)优缺点

优点

①Guarded Suspension模式的Blocker参与者实现的线程挂起与唤醒可以直接使用wait/notify实现,但是后者大大增加了代码出错的概率。

②Guarded Suspension模式中各个参与者仅关注本模式需要解决问题的一个方面,职责高内聚。增加了代码的可读性、可维护性。

缺点

①增加了JVM GC负担。

为了使GuardedAction实例的call方法能够访问guardedMethod参数,需要利用闭包(Closure),GuardedAction实例在guardedMethod中创建,故每次guardedMethod被调用时都有新的GuardedAction实例被创建。

②增加了上下文切换。

只要涉及线程暂挂和唤醒,就会引起频繁的上下文切换。

(4)相关模式

Promise模式—>5

Producer-Consumer模式–>6

8,Active Object(主动对象)模式

(1)概念

①Active Object模式

是一种异步编程模式。相当于android中的异步任务。

通过对方法的调用与方法执行进行解耦来提高并发性。

核心思想:

将任务的调用方和执行方运行在不同的线程中,从而提高并发性。

(2)场景

适用于一个比较耗时的任务(如涉及I/O操作的任务),可以减少不必要的等待师姐。

(3)优点

①提高了并发性,提高了系统吞吐量。

②将任务提交和执行分离,任务的执行策略被封装在Scheduler的实现类中,即使变动也不会影响其它代码,降低了系统的耦合性。

(4)实现

对外暴露异步方法,封装与之相关的上下文、操作、参数为一个对象:方法请求(Method Request)。Method Request存入Active Object模式的缓冲区中,由专门的工作者线程执行相关操作。

①Proxy

负责对外暴露异步方法接口。

–asyncService:

由客户端调用。

创建相应的MethodRequest实例,并将其提交给Scheduler。

返回值是一个Future参与者实例,客户端通过它获取异步任务相应的执行结果。

②MethodRequest

负责将客户端对Proxy的异步方法调用封装成一个对象,该对象保留异步方法的名称、客户端传递来的参数等上下文信息。

–call:根据MethodRequest包含的上下文信息调用Servant实例的相应方法。

③ActivationQueue

缓冲区,临时存储Proxy异步方法被调用时创建的MethodRequest实例。

–enqueue

–dequeue

④Scheduler

负责将Proxy的异步方法所创建的MethodRequest实例存入其维护的缓冲区中,并根据一定的调度策略,对其维护的缓冲区中的MethodRequest实例进行执行。

–enqueue

–dispatch:反复从缓冲区取出MethodRequest实例进行执行。

⑤Servant

负责Proxy所暴露的异步方法的具体实现。

–doSomething:执行异步方法对应的任务。

⑥Future

负责存储和获取Active Object异步方法的执行结果。

–get

–set

(5)相关模式

①Promise模式—>5

Active Object模式的Proxy参与者相当于Promise模式中的Promisor参与者。

asyncService异步方法的返回值Future相当于Promise模式中的Promise参与者。

②Producer-Consumer模式–>6

Active Object模式可以看作Producer-Consumer模式的一个实例。

9,Master-Slave(主仆)模式

(1)概念

Master-Slave模式是一个基于分而治之(Divide and conquer)思想的设计模式。

核心思想:将一个任务分解为若干个语义等同的子任务,由专门的工作者线程来执行这些子任务。通过整合这些子任务的处理结果形成原始任务的处理结果。而这些子任务处理细节对于原始任务来说是不可见的。

(2)场景

①并行计算

提升计算性能。

②容错处理

提高计算的可靠性。

原始任务的处理结果是任意一个Slave实例的成功处理结果(失败的实例不返回结果)。

③计算精度

提高计算精确程度。

返回结果为所有Slave实例中处理结果精确度最低的一个。

(3)实现

在子任务和客户端代码之间引入一个协调性对象(Master)。

①Master

负责原始任务的分解、子任务的派发、子任务处理结果的合并。

–service:对外暴露的接口,用于接收原始任务,返回处理结果。

–splitWork:将原始任务分解。

–callSlaves:将各个子任务派发给Slave实例处理。

–combineResults:整合各个子任务的处理结果。

②Slave

负责子任务的处理。

–subService:异步方法,负责执行子任务的处理逻辑。

(4)优缺点

①优点

提高计算效率,同时实现了信息隐藏。

提高服务器的可维护性、简化客户端代码。

②缺点

实现困难。

(5)问题

①子任务处理结果的收集。

a.存储仓库(Repository)

Master等待各个Slave处理完毕后从存储仓库中获取各个子任务的处理结果。

b.使用Promise模式

②Slave线程的停止

用Two-phase Termination模式 实现处理完分配的任务后停止。

10,Two-phase Termination(两阶段终止)模式

(1)概念

Two-phase Termination模式将停止线程这个动作分解为准备阶段和执行阶段,优雅的停止线程。

①准备阶段

通知目标线程准备停止。

通过调用目标线程的interrupt方法,以期望目标线程能够通过捕获相关异常侦测到该方法的调用,从而中断阻塞、等待状态。

②执行阶段

检查准备阶段设置的线程停止标志和信号,进行适当的清理操作。

(2)场景

优雅的停止线程。

(3)实现

①ThreadOwner

目标线程的拥有者(即创建者)。假定其知道目标线程的工作内容,可以安全地停止目标线程。

–shutdown():由客户端调用。调用目标线程的terminate方法。

②Terminatable

可停止线程。

–terminate():请求目标线程停止。

③AbstractTerminatableThread

可停止的线程。

–terminate()

设置线程停止标志,并发送停止信号给目标线程。

–doTerminate()

留给子类实现线程停止时所需的一些额外操作,如目标线程代码中包含Socket I/O,子类可以在该方法中关闭Socket快速停止线程,而不会使目标线程等待I/O完成才能侦测到线程停止标记。

–doRun()

线程处理逻辑方法 。留给子类实现线程的处理逻辑。相当于Thread.run().

不用关心停止线程的逻辑,因为这个逻辑已经封装在TerminatableThread的run方法中。

–doCleranup

留给子类实现线程停止后进行的清理动作。

④TerminationToken

线程停止标志。

toShutdown用于指示目标线程可以停止了。

reservations用于反映目标线程还有多少数量未完成的任务,以支持等目标线程处理完其任务后再停止。

⑤ConcreteTerminatableThread

AbstractTerminatableThread的实现类。

11,Thread Pool(线程池)模式

(1)概念

①Thread Pool模式

为每个或者每一批任务创建一个线程执行其任务,很奢侈。比较常见的做法是复用一定数量的线程,去执行不断产生的任务。

核心思想:

使用队列对待处理的任务进行缓存,并复用一定数量的工作者线程去取队列中的任务进行执行。

本质:使用有限资源处理相对无限的任务。

②线程泄露(Thread Leak)

指线程池中的工作者线程意外终止,使得线程池中可用的工作者线程变少,如果Thread Leak持续存在,工作者线程会越来越少,最终无法处理提交的任务。

通常造成原因是由于线程对象的run方法中异常处理没有捕获RuntimeException和Error导致run方法意外返回,线程提前终止。

另外,也要对线程等待外部资源的时间进行限制。

(2)场景

为了防止死锁,提交给同一线程池实例执行的任务是相互独立的,而不是彼此有依赖关系的。

(3)实现



其中使用SynchronousQueue作为工作队列。

(4)优点

①抵消线程创建的开销,提高响应性。

创建线程的消耗:a.线程对象本身 b.调用栈所需内存 c.创建依赖于JVM宿主操作系统的本地线程。

Thread Pool模式通过事先创建一部分线程使得被提交的任务可以立即被执行而无需等待工作者线程的创建,从而提高了想影响。

②封装了工作者线程生命周期管理。

线程池本身负责了工作者线程的生命周期的管理,客户端只需要关心任务的提交及处理结果。

③减少了销毁线程的开销。

12,Pipeline(流水线)模式

(1)概念

核心思想:将一个任务处理分解为若干个处理阶段(Stage),其中每个处理阶段的输出作为下一个处理阶段的输入,并且每个阶段都要相应的工作者线程取执行相应计算。

(2)场景

允许子任务间存在依赖关系下实现并行计算。适合处理规模较大的任务。

(3)优缺点

优点:充分利用多核CPU,提高计算效率。

缺点:编码复杂,出现问提不好定位。

13,Half-sync/Half-async(半同步/半异步)模式

(1)概念

通过同步任务和异步任务的共同协作来完成一个计算。

核心思想:如何将系统的任务进行恰当的分解,使各个子任务落入合适的层次中。

Half-sync/Half-async模式是一个分层架构:

①异步任务层

安排低级的任务(如与UI相关的任务)、耗时较短的任务。

原因:减少了客户端的等待,提升了系统吞吐率。

②同步任务层

安排高级的任务(如数据库访问)、耗时较长的任务。

原因:在不影响客户端的情况下,保持了同步编程的简单性。

③队列层

前两层之间的协作通过队列层解耦,负责这两层之间的数据交换。

(2)场景

某计算同时涉及低级(或耗时短)任务和高级(或耗时长)任务。

(3)优缺点

优点:保持了同步编程的简单,又有异步编程再提高系统并发性方面的优势。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: