在软件工程中,设计模式是针对某一类共同问题的解决方案。这种解决方案被多次使用,而且已 经被证明是针对该类问题的最优解决方案。每当你需要解决这其中的某个问题,就可以使用它们来避 免做重复工作。其中,单例模式(Singleton)和工厂模式(Factory)是几乎每个应用程序中都要用到 的通用设计模式。 并发处理也有其自己的设计模式。本节,我们将介绍一些最常用的并发设计模式,以及它们的 Java 语言实现。
信号模式
这种设计模式介绍了如何实现某一任务向另一任务通告某一事件的情形。实现这种设计模式最简 单的方式是采用信号量或者互斥,使用 Java 语言中的 ReentrantLock 类或 Semaphore 类即可,甚 至可以采用 Object 类中的 wait()方法和 notify()方法。
会合模式
这种设计模式是信号模式的推广。在这种情况下,第一个任务将等待第二个任务的某一事件,而 第二个任务又在等待第一个任务的某一事件。其解决方案和信号模式非常相似,只不过在这种情况下, 你必须使用两个对象而不是一个。
互斥模式
互斥这种机制可以用来实现临界段,确保操作相互排斥。这就是说,一次只有一个任务可以执行 由互斥机制保护的代码片段。在 Java 中,你可以使用 synchronized 关键字(这允许你保护一段代 码或者一个完整的方法)、ReentrantLock 类或者 Semaphore 类来实现一个临界段。
多元复用模式
多元复用设计模式是互斥机制的推广。在这种情形下,规定数目的任务可以同时执行临界段。这 很有用,例如,当你拥有某一资源的多个副本时。在 Java 中实现这种设计模式最简单的方式是使用 Semaphore 类,并且使用可同时执行临界段的任务数来初始化该类。
栅栏模式
这种设计模式解释了如何在某一共同点上实现任务同步的情形。每个任务都必须等到所有任务 都到达同步点后才能继续执行。Java 并发 API 提供了 CyclicBarrier 类,它是这种设计模式的一 个实现。
双重检查锁定模式
当你获得某个锁之后要检查某项条件时,这种设计模式可以为解决该问题提供方案。如果该条件 为假,你实际上也已经花费了获取到理想的锁所需的开销。对象的延迟初始化就是针对这种情形的例 子。如果你有一个类实现了单例设计模式,那可能会有如下这样的代码。
读-写锁模式
当你使用锁来保护对某个共享变量的访问时,只有一个任务可以访问该变量,这和你将要对该变 量实施的操作是相互独立的。有时,你的变量需要修改的次数很少,却需要读取很多次。这种情况下, 12 锁的性能就会比较差了,因为所有读操作都可以并发进行而不会带来任何问题。为解决这样的问题, 出现了读写锁设计模式。这种模式定义了一种特殊的锁,它含有两个内部锁:一个用于读操作,而 另一个用于写操作。该锁的行为特点如下所示。
- 如果一个任务正在执行读操作而另一任务想要进行另一个读操作,那么另一任务可以进行该 操作。
- 如果一个任务正在执行读操作而另一任务想要进行写操作,那么另一任务将被阻塞,直到所 有的读取方都完成操作为止。
- 如果一个任务正在执行写操作而另一任务想要执行另一操作(读或者写),那么另一任务将被 阻塞,直到写入方完成操作为止。
Java 并发 API 中含有 ReentrantReadWriteLock 类,该类实现了这种设计模式。如果你想从头 开始实现该设计模式,就必须非常注意读任务和写任务之间的优先级。如果有太多读任务存在,那么 写任务等待的时间就会很长。
线程池模式
这种设计模式试图减少为执行每个任务而创建线程所引入的开销。该模式由一个线程集合和一个 待执行的任务队列构成。线程集合通常具有固定大小。当一个线程完成了某个任务的执行时,它本身 并不会结束执行,它要寻找队列中的另一个任务。如果存在另一个任务,那么它将执行该任务。如果 不存在另一个任务,那么该线程将一直等待,直到有任务插入队列中为止,但是线程本身不会被终结。 Java 并发 API 包含一些实现 ExecutorService 接口的类,该接口内部采用了一个线程池。
线程局部存储模式
这种设计模式定义了如何使用局部从属于任务的全局变量或静态变量。当在某个类中有一个静态 属性时,那么该类的所有对象都会访问该属性的同一存在。如果使用了线程局部存储,则每个线程都 会访问该变量的一个不同实例。 Java 并发 API 包含了 ThreadLocal 类,该类实现了这种设计模式。
参考资料
文档信息
- 本文作者:Bob.Zhu
- 本文链接:https://adolphor.github.io/2021/07/27/07-concurrent-design-patterns/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)