本篇我们来探讨一下并发设计模型。并发系统可以使用不同的并发模型来实现,这些模型描述了系统中的线程如何协作完成并发任务。不同的并发模型以不同的方式拆分任务,线程可以以不同的方式进行通信和协作。并发模型与分布式系统非常相似。并发模型实际上与分布式系统模型非常相似。在并发模型中,线程相互通信,而在分布式系统模型中,进程相互通信。然而,从本质上讲,进程和线程也非常相似。这就是并发模型与分布式模型非常相似的原因。分布式系统通常比并发系统面临更多的挑战和问题,比如进程通信,网络异常,或者远程机器挂了等等。但是并发模型也会面临CPU故障,网卡故障,硬盘故障等问题。因为并发模型和分布式模型很相似,可以互相学习。例如,线程分配的模型类似于分布式系统环境中的负载平衡模型。其实说白了,分布式模型的思想是从并发模型推演和发展而来的。理解双态并发模型的一个重要方面是线程是否应该共享状态、共享状态或独立状态。共享状态意味着某些状态在不同线程之间共享。状态实际上是数据,例如一个或多个对象。当线程想要共享数据时,会导致竞争条件或死锁等问题。当然,这些问题只是可能的,具体的实现取决于你是否安全地使用和访问共享对象。独立状态意味着状态不会在多个线程之间共享。如果线程需要通信,它们可以访问不可变对象来实现。这是避免并发问题最有效的方法之一,如下图所示使用独立状态使我们的设计更简单,因为只有一个线程可以访问对象,即使交换对象也是不可变的目的。并发模型ParallelWorker第一个并发模型是并行worker模型。客户端将任务交给代理(Delegator),然后代理将工作分配给不同的工人(worker)。如下图所示的parallelworker的核心思想是它主要有两个进程,agent和worker。Delegator负责接收来自客户端的任务,并将任务交付给具体的Worker进行处理。Worker处理完成后,将结果Return发送给Delegator,Delegator收到Worker处理后的结果后,进行汇总,然后交给客户端。parallelworker模型是Java并发模型中非常常见的一种。java.util.concurrent包中的许多并发工具都使用这种模型。ParallelWorker的优点ParallelWorker模型一个很明显的特点就是简单易懂。为了提高系统的并行度,可以添加多个Worker来完成任务。并行Worker模型的另一个优点是它将一个任务拆分成多个小任务并发执行。Delegator收到Worker的处理结果后返回给Client。Worker->Delegator->Client整个过程是异步的。ParallelWorker的缺点同样,ParallelWorker模式也有一些隐藏的缺点。共享状态会变得非常复杂。实际的并行Worker比我们在图中显示的更复杂。主要原因是并行Worker通常访问内存或共享数据库。中的一些共享数据。这些共享状态可能会使用一些工作队列来保存业务数据、数据缓存、数据库连接池等。在线程通信中,线程需要保证共享状态是否可以被其他线程共享,而不是仅仅停留在CPU缓存中使其可用。当然,这些都是程序员在设计时需要考虑的问题。线程需要避免竞争条件、死锁和许多其他由共享状态引起的并发问题。当多个线程访问共享数据时,会失去并发性,因为操作系统必须保证只有一个线程可以访问数据,这会导致共享数据的争用和抢占。不抢占资源的线程会阻塞。现代非阻塞并发算法可以减少竞争并提高性能,但非阻塞算法更难实现。持久数据结构是另一种选择。持久数据结构在修改后始终保留以前的版本。因此,如果多个线程同时修改一个持久数据结构,并且一个线程修改它,则修改线程将获得对新数据结构的引用。尽管持久数据结构是一种新的解决方案,但这种方法的实现存在一些问题。例如,持久列表会将新元素添加到列表的开头并返回对添加的新元素的引用。但是其他线程仍然只持有对列表中先前第一个元素的引用,他们看不到新添加的元素。链表(LinkedList)等持久化数据结构在硬件性能方面表现不佳。列表中的每个元素都是一个对象,这些对象分散在计算机的内存中。现代CPU上的顺序访问往往要快得多,因此使用顺序访问的数据结构(如数组)可以获得更高的性能。CPU缓存可以将一个大的矩阵块加载到缓存中,加载后让CPU直接访问CPU缓存中的数据。对于链表,将元素分布在整个RAM中几乎是不可能的。Statelessworker共享状态可以被其他线程修改,因此worker每次对共享状态进行操作时都必须重新读取,以确保在副本上正确工作。不在线程内保持状态的工作者成为无状态工作者。作业顺序不确定的并行工作模型的另一个缺点是作业的顺序是不确定的,不能保证哪个作业先执行或最后执行。任务A先于任务B分配给工人,但任务B可能先于任务A执行。流水线的第二种并发模型就是我们在生产车间经常遇到的流水线并发模型。下面是流水线设计模型的流程图。这种组织结构就像工厂流水线上的工人。每个工人只完成整个工作的一部分,完成一个部分后,工人将工作转发给下一个工人。每个程序都在自己的线程中运行,并且彼此不共享状态。此模型也称为无共享并发模型。使用管道的并发模型通常是为非阻塞I/O设计的,即工作人员在没有分配任务时做其他工作。非阻塞I/O是指当一个worker开始一个I/O操作时,比如从网络读取一个文件,worker不等待I/O调用完成。等待I/O很耗时,因为I/O操作很慢。在等待I/O的同时,CPU可以做其他事情,I/O操作的结果会传递给下一个worker。以下是非阻塞I/O的流程图。在实际情况下,任务通常不会沿着装配线流动。由于大部分程序需要做的事情很多,需要根据完成的不同任务在不同的worker之间流转,如下图所示的任务也可能需要多个worker参与完成reactive-event-driven系统使用管道模型的系统有时称为反应式或事件驱动系统。该模型响应外部事件,这些事件可能是某个HTTP请求或某个文件被加载到内存中。Actor模型在Actor模型中,每个Actor其实就是一个Worker,每个Actor都可以处理任务。简单来说,Actor模型是一个并发模型,它定义了一系列关于系统组件应该如何行动和交互的通用规则。使用这套规则的最著名的编程语言是Erlang。参与者参与者响应收到的消息,然后可以在准备接收下一条消息时创建更多参与者或发送更多消息。Channels模型在Channel模型中,工人通常不直接交流。相反,他们通常将事件发送到不同的通道(Channel),然后其他工作人员可以在这些通道上获取消息。下面是Channel的模型图有时候worker不需要知道下一个worker是谁,只需要将作者写进channel,监听channel的worker可以订阅或者取消订阅,降低了之间的耦合度工人。流水线设计的优点与并行设计模型相比,流水线模型具有一些优点。具体优点如下。没有共享状态是因为pipeline设计可以保证worker处理完成后传递给下一个worker,所以worker和worker之间不需要共享任何状态,也不需要考虑并发问题并发引起的。您甚至可以将每个worker视为单线程实现。有状态工作者因为工作者知道没有其他线程修改它们自己的数据,所以管道设计中的工作者是有状态的。有状态意味着他们可以将他们需要操作的数据保存在内存中。Stateful通常比stateless快。更好的硬件集成,因为您可以将管道视为单线程,而单线程作业的优势在于它可以像硬件一样工作。因为有状态工作者通常在CPU中缓存数据,所以这允许更快地访问缓存数据。为了让任务更高效,可以对流水线并发模型中的任务进行排序,一般用于日志的写入和恢复。流水线设计的缺点流水线并发模型的缺点是任务涉及多个工作人员,因此可能分布在项目代码中的多个类中。因此很难确定每个工人正在执行的任务。流水线也更难编写代码,设计具有许多嵌套回调处理程序的代码通常被称为回调地狱。很难通过调试追踪回调地狱。FunctionalParallel函数式并行模型是最近提出的一种并发模型,其基本思想是使用函数调用来实现。消息传递相当于函数调用。传递给函数的参数被复制,因此函数外部的任何实体都不能操作函数内部的数据。这使得函数执行起来像原子操作。每个函数调用都可以独立于任何其他函数调用执行。当每个函数调用独立执行时,每个函数都可以在单独的CPU上执行。也就是说,函数式并行相当于每个CPU独立执行自己的任务。JDK1.7中的ForkAndJoinPool类实现了函数式并行。Java8提出了流的概念,使用并行流也可以实现大量集合的迭代。函数式并行的难点在于知道函数的调用过程,哪些CPU执行哪些函数。跨CPU的函数调用会带来额外的开销。本文转载自微信公众号“JavaBuilder”,可通过以下二维码关注。如需转载本文,请联系Java开发者公众号。
