并发设计模式
在软件工程中,设计模式是针对某一类共同问题的解决方案。
这种解决方案被多次使用,而且已经被证明是针对该类问题的最优解决方案。
每当你需要解决这其中的某个问题,就可以使用它们来避免做重复工作。
其中, 单例模式(Singleton)和 工厂模式(Factory)是几乎每个应用程序中都要用到的通用设计模式。
信号模式
这种设计模式介绍了如何实现某一任务向另一任务通告某一事件的情形。
实现这种设计模式最简单的方式是采用信号量或者互斥,使用 Java 语言中的 ReentrantLock 类或 Semaphore 类即可,甚至可以采用 Object 类中的 wait() 方法和 notify() 方法。
public void task1() {
section1();
commonObject.notify();
}
public void task2() {
commonObject.wait();
section2();
}
在上述情况下, section2() 方法总是在 section1() 方法之后执行。
会合模式
这种设计模式是信号模式的推广。
在这种情况下,第一个任务将等待第二个任务的某一事件,而第二个任务又在等待第一个任务的某一事件。
其解决方案和信号模式非常相似,只不过在这种情况下,你必须使用两个对象而不是一个。
public void task1() {
section1_1();
commonObject1.notify();
commonObject2.wait();
section1_2();
}
public void task2() {
section2_1();
commonObject2.notify();
commonObject1.wait();
section2_2();
}
在上述情况下, section2_2() 方法总是会在 section1_1() 方法之后执行,而 section1_2()方法总是会在 section2_1() 方法之后执行。
仔细想想就会发现,如果你将对 wait() 方法的调用放在对 notify() 方法的调用之前,那么就会出现死锁。
互斥模式
互斥这种机制可以用来实现临界段,确保操作相互排斥。
这就是说,一次只有一个任务可以执行由互斥机制保护的代码片段。
在 Java 中,你可以使用 synchronized 关键字(这允许你保护一段代码或者一个完整的方法)、 ReentrantLock 类或者 Semaphore 类来实现一个临界段。
public void task() {
preCriticalSection();
try {
lockObject.lock() // 临界段开始
criticalSection();
} catch (Exception e) {
} finally {
lockObject.unlock(); // 临界段结束
postCriticalSection();
}
}
多元复用模式
多元复用设计模式是互斥机制的推广。
在这种情形下,规定数目的任务可以同时执行临界段。
这很有用,例如,当你拥有某一资源的多个副本时。
在 Java 中实现这种设计模式最简单的方式是使用Semaphore 类,并且使用可同时执行临界段的任务数来初始化该类。
public void task() {
preCriticalSection();
semaphoreObject.acquire();
criticalSection();
semaphoreObject.release();
postCriticalSection();
}
栅栏模式
这种设计模式解释了如何在某一共同点上实现任务同步的情形。
每个任务都必须等到所有任务都到达同步点后才能继续执行。
Java 并发 API 提供了 CyclicBarrier 类,它是这种设计模式的一个实现。
public void task() {
preSyncPoint();
barrierObject.await();
postSyncPoint();
}
双重检查锁定模式
当你获得某个锁之后要检查某项条件时,这种设计模式可以为解决该问题提供方案。
如果该条件为假,你实际上也已经花费了获取到理想的锁所需的开销。
对象的延迟初始化就是针对这种情形的例子。
方法一:单例设计模式。
存在问题:如果该条件为假,你实际上也已经花费了获取到理想的锁所需的开销。
public class Singleton{
private static Singleton reference;
private static final Lock lock=new ReentrantLock();
public static Singleton getReference() {
try {
lock.lock();
if (reference==null) {
reference=new Object();
}
} catch (Exception e) {
System.out.println(e);
} finally {
lock.unlock();
}
return reference;
}
}
方法二:在条件之中包含锁。
存在问题:如果两个任务同时检查条件,你将要创建两个对象。
public class Singleton{
private Object reference;
private Lock lock=new ReentrantLock();
public Object getReference() {
if (reference==null) {
lock.lock();
if (reference == null) {
reference=new Object();
}
lock.unlock();
}
return reference;
}
}
解决这一问题的最佳方案就是不使用任何显式的同步机制。
方法三:不使用任何显式的同步机制。
public class Singleton {
private static class LazySingleton {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getSingleton() {
return LazySingleton.INSTANCE;
}
}
读-写锁模式
当你使用锁来保护对某个共享变量的访问时,只有一个任务可以访问该变量,这和你将要对该变量实施的操作是相互独立的。
有时,你的变量需要修改的次数很少,却需要读取很多次。
这种情况下,锁的性能就会比较差了,因为所有读操作都可以并发进行而不会带来任何问题。
为解决这样的问题,出现了读-写锁设计模式。这种模式定义了一种特殊的锁,它含有两个内部锁:一个用于读操作,而另一个用于写操作。
该锁的行为特点如下所示:
-
如果一个任务正在执行读操作而另一任务想要进行另一个读操作,那么另一任务可以进行该操作。
- 如果一个任务正在执行读操作而另一任务想要进行写操作,那么另一任务将被阻塞,直到所有的读取方都完成操作为止。
- 如果一个任务正在执行写操作而另一任务想要执行另一操作(读或者写),那么另一任务将被阻塞,直到写入方完成操作为止。
Java 并发 API 中含有 ReentrantReadWriteLock 类,该类实现了这种设计模式。
如果你想从头开始实现该设计模式,就必须非常注意读任务和写任务之间的优先级。
如果有太多读任务存在,那么写任务等待的时间就会很长。
线程池模式
这种设计模式试图减少为执行每个任务而创建线程所引入的开销。
该模式由一个线程集合和一个待执行的任务队列构成。
线程集合通常具有固定大小。当一个线程完成了某个任务的执行时,它本身并不会结束执行,它要寻找队列中的另一个任务。
如果存在另一个任务,那么它将执行该任务。
如果不存在另一个任务,那么该线程将一直等待,直到有任务插入队列中为止,但是线程本身不会被终结。
Java 并发 API 包含一些实现 ExecutorService 接口的类,该接口内部采用了一个线程池。
Java通过Executors提供四种线程池,分别为:
-
newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 - newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 - newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。 - newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
线程局部存储模式
这种设计模式定义了如何使用局部从属于任务的全局变量或静态变量。
当在某个类中有一个静态属性时,那么该类的所有对象都会访问该属性的同一存在。
如果使用了线程局部存储,则每个线程都会访问该变量的一个不同实例。
Java 并发 API 包含了 ThreadLocal 类,该类实现了这种设计模式。