知识来源:《图解Java多线程设计模式》,作者是结城浩。
(1)Single Threaded Execution模式
问题:某些非线程安全的类的实例如果同时被多个线程访问会导致失去安全性。
解决方案:首先严格规定实例的临界区(即不可同时被多个线程访问的部分,一般为各类读写操作),之后施加保护,保证临界区内的代码一次只能由一个线程访问。
实现:将临界区置于synchronized同步方法或是synchronized()同步块中。
(2)Immutable模式
问题:对于某些可能被频繁访问的对象,如果利用同步限制其访问权限会导致严重的性能损失。
解决方案:将类设计成其对象一旦初始化完毕后就无法改变的模式。
实现:
- 保证所有域都是private和final的;
- 不提供setter方法;
- 保证对象的所有域本身是Immutable的。(即使保存的引用是final的,也不能保证引用指向的对象不会发生改变)
(3)Guarded Suspension模式
问题:某些对象可能需要满足一定条件才能访问,在未满足条件的状况下访问会出错。
解决方案:确定该对象的守护条件(即在满足该条件的情况下才能访问),并在每次访问对象之前确保守护条件成立。
实现:
public synchronized void visit(){
//确保守护条件成立
while(!守护条件成立){
try{
wait();
}catch (InterruptedException e) {
}
}
//实际访问对象的方法
doVisit();
}
注意点:
- 要用while循环确保守护条件成立,不能使用if。
- 在某些可能导致守护条件改变的方法中,要调用notifyAll()方法来唤醒之前由于守护条件不成立而处于等待状态的线程。
- 整个判断——访问的过程需要被synchronized关键字保护,否则可能出现多个线程同时穿过守护条件的情况发生。
- 关于InterruptedException异常,可以将其在while循环内处理掉,实现无限阻塞;也可以让访问方法抛出,实现在调用interrupt()时从访问方法中跳出。
(4)Balking模式
问题:某些工作只在特定条件满足的情况下才需要做,当条件不满足时只要直接返回即可。
解决方案:工作前进行一下判断,若不满足条件则直接返回,不在原地阻塞。
实现:
public synchronized void work(){
//判断条件是否成立,不成立则直接返回
if(!条件成立){
return;
}
//进行工作
doWork();
}
需要使用synchronized关键字保证不会出现多个线程同时穿过条件判断的情况。
(5)Timeout模式
问题:在(3)的基础之上,希望只阻塞有限的时间,而不会无限等待。
解决方案:每次等待前确定最多还需要等待的时长。
实现:
public synchronized void visit() throws TimeoutException{
long start = System.currentTimeMillis();
//确保守护条件成立
while(!守护条件成立){
long now = System.currentTimeMillis();
long rest = now - start;
if(rest <= 0){
throw new TimeoutException();
}
try{
wait();
}catch (InterruptedException e) {
}
}
//实际访问对象的方法
doVisit();
}
(6)Producer-Consumer模式
问题:需要从某个线程(Producer)向其他线程(Consumer)安全地传递对象。
解决方案:在Producer与Consumer中间设置一个中转站Channel,由Channel控制对象的收发。
实现:使用concurrent包的BlockingQueue系列可以简单地实现“无对象时阻塞,有对象时唤醒”以及“队列满时阻塞,有空位时唤醒”两个最重要的需求。
(7)Read-Write Lock模式
问题:让读取数据的线程能够并行,而读取数据的线程与修改数据的线程互斥。
解决方案:设计一个Lock对象来控制读写线程对数据的访问权限。
实现:
public class ReadWriteLock {
private int readingReader = 0;
private int writingWriter = 0;
private int waitingWriter = 0;
private boolean preferWriter = true;
//每次读取前调用
public synchronized void readLock() throws InterruptedException{
while(writingWriter > 0 || (preferWriter && waitingWriter > 0)){
wait();
}
readingReader++;
}
//每次读取后调用
public synchronized void readUnlock(){
readingReader--;
preferWriter = true;
notifyAll();
}
//每次修改前调用
public synchronized void writeLock() throws InterruptedException{
waitingWriter++;
try {
while(readingReader > 0 || writingWriter > 0){
wait();
}
} finally {
waitingWriter--;
}
writingWriter++;
}
//每次修改后调用
public synchronized void writeUnlock(){
writingWriter--;
preferWriter = false;
notifyAll();
}
}
也可以利用concurrent.lock包的ReentrantReadWriteLock实现,每个ReentrantReadWriteLock对象内包含一把ReadLock和一把WriteLock,之后和上面的使用方法基本相同。
(8)Thread-Per-Message模式
问题:如果由主线程直接处理某些耗时长的工作的话,响应性会大幅下降。
解决方案:为每项工作启动一个新线程,在新线程中完成工作。
实现:使用匿名内部类现场创建一个线程,或是使用预先设计好的继承自Thread的类。
(9)Worker Thread模式(或叫Thread Pool模式)
问题:程序运行过程中会出现许多轻量级的工作,每次都创建新线程的话会损失性能。
解决方案:设计一种工作线程,这种线程会不断地从工作池中取出工作并完成。
实现:需要设计两个类:Channel——负责工作的收发;WorkerThread:负责不断地从Channel中获取工作并执行。实际编程中推荐使用concurrent包的Executors提供的各类线程池。
(10)Future模式
问题:需要线程执行的工作的处理结果。
解决方案:将工作提交给线程时先返回一个Future对象,稍后可以从Future对象中获得处理结果。工作线程除了要执行工作之外,还需要负责将结果设置到Future对象中。
实现:一般借助ExecutorService实现类,通过submit()传递一个Callable<T>对象获取Future<T>对象,稍后可以利用该对象获取结果。
(11)Two-Phase Termination模式
问题:需要提供一种方法安全有效地结束线程。
解决方案:在Thread子类中添加一个标志位isShutdownRequested,默认为false,并提供设置该标志位的方法。线程run()方法的过程中对该标志为进行判断,如果true则跳出,并利用finally代码块进行清理工作。
public abstract class StopableThread extends Thread {
private volatile boolean isShutdownRequested = false;
public final void shutdown(){
isShutdownRequested = true;
interrupt();
}
public final boolean isShutdownRequested() {
return isShutdownRequested;
}
@Override
public void run() {
try {
while(!isShutdownRequested()){
doWork();
}
} catch (InterruptedException e) {
} finally {
doShutdown();
}
}
protected abstract void doWork() throws InterruptedException;
protected abstract void doShutdown();
// public static void main(String[] args) {
// StopableThread thread = new StopableThread() {
//
// @Override
// protected void doWork() throws InterruptedException {
// System.out.println("running...");
// sleep(500);
// }
//
// @Override
// protected void doShutdown() {
// System.out.println("shutdown!");
// }
// };
// thread.start();
//
// try {
// Thread.sleep(3000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//
// thread.shutdown();
// }
}
注意点:
- (重要)不能简单地使用isInterrupted()方法代替标志位。Thread的interrupt()方法的文档中有写到,如果调用interrupt()方法时线程处于sleep()、wait()、join()导致的挂起状态中,线程的interrupt状态会被清除,并会抛出一个InterruptedException。如果线程当前执行的代码块忽略了InterruptedException,线程就不会终止,isInterrupted()也会始终返回false。
- 由于标志位可能被多个线程访问,因而必须设置成volatile的。
(11)Thread-Specific Storage模式
问题:每个线程都有一些属于自己的数据和对象,希望能够在不互相干扰的条件下储存起来。
解决方案:设计一个Map用于存取对象,使用的key是线程本身。
实现:利用java.lang.ThreadLocal<T>类,该类提供了set()与get()方法,用于存储和获取T类型的对象。过程中使用的key是当前线程,因此多线程状况下也不会发生冲突。