1.5 并发设计模式

在软件工程中,设计模式是针对某一类共同问题的解决方案。这种解决方案被多次使用,而且已经被证明是针对该类问题的最优解决方案。每当你需要解决这其中的某个问题,就可以使用它们来避免做重复工作。其中,单例模式(Singleton)和工厂模式(Factory)是几乎每个应用程序中都要用到的通用设计模式。

并发处理也有其自己的设计模式。本节,我们将介绍一些最常用的并发设计模式,以及它们的Java语言实现。

1.5.1 信号模式

这种设计模式介绍了如何实现某一任务向另一任务通告某一事件的情形。实现这种设计模式最简单的方式是采用信号量或者互斥,使用Java语言中的ReentrantLock类或Semaphore类即可,甚至可以采用Object类中的wait()方法和notify()方法。

请看下面的例子。

  1. public void task1() {
  2. section1();
  3. commonObject.notify();
  4. }
  5. public void task2() {
  6. commonObject.wait();
  7. section2();
  8. }

在上述情况下,section2()方法总是在section1()方法之后执行。

1.5.2 会合模式

这种设计模式是信号模式的推广。在这种情况下,第一个任务将等待第二个任务的某一事件,而第二个任务又在等待第一个任务的某一事件。其解决方案和信号模式非常相似,只不过在这种情况下,你必须使用两个对象而不是一个。

请看下面的例子。

  1. public void task1() {
  2. section1_1();
  3. commonObject1.notify();
  4. commonObject2.wait();
  5. section1_2();
  6. }
  7. public void task2() {
  8. section2_1();
  9. commonObject2.notify();
  10. commonObject1.wait();
  11. section2_2();
  12. }

在上述情况下,section2_2()方法总是会在section1_1()方法之后执行,而section1_2()方法总是会在section2_1()方法之后执行。仔细想想就会发现,如果你将对wait()方法的调用放在对notify()方法的调用之前,那么就会出现死锁。

1.5.3 互斥模式

互斥这种机制可以用来实现临界段,确保操作相互排斥。这就是说,一次只有一个任务可以执行由互斥机制保护的代码片段。在Java中,你可以使用synchronized关键字(这允许你保护一段代码或者一个完整的方法)、ReentrantLock类或者Semaphore类来实现一个临界段。

让我们看看下面的例子。

  1. public void task() {
  2. preCriticalSection();
  3. try {
  4. lockObject.lock() // 临界段开始
  5. criticalSection();
  6. } catch (Exception e) {
  7. } finally {
  8. lockObject.unlock(); // 临界段结束
  9. postCriticalSection();
  10. }

1.5.4 多元复用模式

多元复用设计模式是互斥机制的推广。在这种情形下,规定数目的任务可以同时执行临界段。这很有用,例如,当你拥有某一资源的多个副本时。在Java中实现这种设计模式最简单的方式是使用Semaphore类,并且使用可同时执行临界段的任务数来初始化该类。

请看如下示例。

  1. public void task() {
  2. preCriticalSection();
  3. semaphoreObject.acquire();
  4. criticalSection();
  5. semaphoreObject.release();
  6. postCriticalSection();
  7. }

1.5.5 栅栏模式

这种设计模式解释了如何在某一共同点上实现任务同步的情形。每个任务都必须等到所有任务都到达同步点后才能继续执行。Java并发API提供了CyclicBarrier类,它是这种设计模式的一个实现。

请看下面的例子。

  1. public void task() {
  2. preSyncPoint();
  3. barrierObject.await();
  4. postSyncPoint();
  5. }

1.5.6 双重检查锁定模式

当你获得某个锁之后要检查某项条件时,这种设计模式可以为解决该问题提供方案。如果该条件为假,你实际上也已经花费了获取到理想的锁所需的开销。对象的延迟初始化就是针对这种情形的例子。如果你有一个类实现了单例设计模式,那可能会有如下这样的代码。

  1. public class Singleton{
  2. private static Singleton reference;
  3. private static final Lock lock=new ReentrantLock();
  4. public static Singleton getReference() {
  5. try {
  6. lock.lock();
  7. if (reference==null) {
  8. reference=new Object();
  9. }
  10. } catch (Exception e) {
  11. System.out.println(e);
  12. } finally {
  13. lock.unlock();
  14. }
  15. return reference;
  16. }
  17. }

一种可能的解决方案就是在条件之中包含锁。

  1. public class Singleton{
  2. private Object reference;
  3. private Lock lock=new ReentrantLock();
  4. public Object getReference() {
  5. if (reference==null) {
  6. lock.lock();
  7. if (reference == null) {
  8. reference=new Object();
  9. }
  10. lock.unlock();
  11. }
  12. return reference;
  13. }
  14. }

该解决方案仍然存在问题。如果两个任务同时检查条件,你将要创建两个对象。解决这一问题的最佳方案就是不使用任何显式的同步机制。

  1. public class Singleton {
  2. private static class LazySingleton {
  3. private static final Singleton INSTANCE = new Singleton();
  4. }
  5. public static Singleton getSingleton() {
  6. return LazySingleton.INSTANCE;
  7. }
  8. }

1.5.7 读-写锁模式

当你使用锁来保护对某个共享变量的访问时,只有一个任务可以访问该变量,这和你将要对该变量实施的操作是相互独立的。有时,你的变量需要修改的次数很少,却需要读取很多次。这种情况下,锁的性能就会比较差了,因为所有读操作都可以并发进行而不会带来任何问题。为解决这样的问题,出现了读-写锁设计模式。这种模式定义了一种特殊的锁,它含有两个内部锁:一个用于读操作,而另一个用于写操作。该锁的行为特点如下所示。

  • 如果一个任务正在执行读操作而另一任务想要进行另一个读操作,那么另一任务可以进行该操作。
  • 如果一个任务正在执行读操作而另一任务想要进行写操作,那么另一任务将被阻塞,直到所有的读取方都完成操作为止。
  • 如果一个任务正在执行写操作而另一任务想要执行另一操作(读或者写),那么另一任务将被阻塞,直到写入方完成操作为止。

Java并发API中含有ReentrantReadWriteLock类,该类实现了这种设计模式。如果你想从头开始实现该设计模式,就必须非常注意读任务和写任务之间的优先级。如果有太多读任务存在,那么写任务等待的时间就会很长。

1.5.8 线程池模式

这种设计模式试图减少为执行每个任务而创建线程所引入的开销。该模式由一个线程集合和一个待执行的任务队列构成。线程集合通常具有固定大小。当一个线程完成了某个任务的执行时,它本身并不会结束执行,它要寻找队列中的另一个任务。如果存在另一个任务,那么它将执行该任务。如果不存在另一个任务,那么该线程将一直等待,直到有任务插入队列中为止,但是线程本身不会被终结。

Java并发API包含一些实现ExecutorService接口的类,该接口内部采用了一个线程池。

1.5.9 线程局部存储模式

这种设计模式定义了如何使用局部从属于任务的全局变量或静态变量。当在某个类中有一个静态属性时,那么该类的所有对象都会访问该属性的同一存在。如果使用了线程局部存储,则每个线程都会访问该变量的一个不同实例。

Java并发API包含了ThreadLocal类,该类实现了这种设计模式。