vlambda博客
学习文章列表

深度·连载 | Java并发与多线程模型:并发模型

并发系统可以由不同的并发模型实现。并发模型指定了系统中线程如何协作完成任务。不同的模型划分任务的方法不同,线程之间也可以以不同的方式进行通信和协作。


本文参考资料:

http://tutorials.jenkov.com/java-concurrency/index.html

• https://blog.csdn.net/huyongl1989/article

/details/90680118

•《深入理解java虚拟机 周志明》

https://en.wikipedia.org/wiki/Concurrency

(computerscience)


并发模型



并发模型和分布式系统的相似之处




并发模型和分布式系统中使用的体系结构有一定的相似之处。在并发系统中,不同的线程批次通信。在分布式系统中,不同的进程相互通信(可能在不同的计算机上)。本质上,线程和进程特别相似。当然,分布式系统有着更多的挑战,网络可能会失败,远程计算机或进程已关闭等。理论上,线程也会因为CPU故障,网卡故障,磁盘故障等遇到类似的问题。虽然可能性不大。


因为两者是有一定的相似性的,所以可以相互借鉴一些设计思想。例如,用于在工作程序(线程)之间分配工作的模型,可以用于分布式系统中的负载均衡。一些error处理技术,像日志,故障转移,请求的幂等性等。


Shared State vs. Separate State




并发模型最重要的一点是,组件和线程被设计成是共享状态还是分离状态。


共享状态以为这不同线程将在他们之间共享某些状态(State)。状态通常是指数据,一个或者多个对象。当线程共享状态时,可能会出现争用条件和死锁的问题。当然,这取决于线程如何使用和访问共享对象。



分离状态表示系统中的不同线程在它们之间不共享任何状态。万一不同的线程需要通信,它们可以通过在它们之间交换不可变对象或通过在它们之间发送对象(或数据)的副本来进行通信。因此,当没有两个线程写入同一对象(数据/状态)时,可以避免大多数常见的并发问题。


深度·连载 | Java并发与多线程模型:并发模型


使用分离状态并发设计通常可以使代码的某些部分更易于实现和推理,因为您知道只有一个线程将写入给定对象。您不必担心并发访问该对象。但是,您可能需要对全局应用程序设计进行更深入的思考,才能使用单独的状态并发。我觉得这是值得的。我个人更喜欢分离状态并发设计。


Parallel Workers




第一个并发模型叫做并行工人(Parallel Workers).因为工作将会指派给不同的workers。如下图:


深度·连载 | Java并发与多线程模型:并发模型


在这个模型中,委托人将传入的作业分配给不同的工人。每个工人完成自己的工作。这些工作程序并行工作,在不同的线程中运行,并可能在不同的cpu中运行。


这个模型是java应用程序中最常应用的模型之一,java.util.concurrent包下许多的并行设计都采用这个模型。


Parallel Workers优点




易于理解;要增加程序的并行化,只需要添加worker就行。


一个CPU一个线程实在太少了,避免CPU的浪费。


Parallel Workers缺点




Shared State Can Get Complex


实际上并行工人模型要比上面阐述的复杂一些。工人们需要访问一些共享的数据,在内存中或者共享的数据库中。如下图:


深度·连载 | Java并发与多线程模型:并发模型


这种共享状态,会使得程序变得复杂。线程修改共享数据时,需要确保这个操作能被其他线程看到(将数据推送到主存中,而不仅仅停留在CPU和CPU的缓存中)。线程需要避免征用条件,死锁等共享状态的并发问题。


当线程在访问共享数据时,会互相等待,也就是阻塞。这也就意味着在一段时间内,只有一个或者有限的一组可以访问线程。这可能导致共享数据的争用。


现在的non-blocking concurrency algorithms无阻塞并发算法可以减少争用并提高效率,但是很难实现。


持久化数据结构是另外一种选择。持久数据结构在修改后始终保留其自身的先前版本。因此,如果多个线程指向相同的持久数据结构,并且一个线程对其进行了修改,则修改线程将获得对新结构的引用。所有其他线程保留对旧结构的引用,该旧结构仍保持不变,因此是一致的。在Scala编程中,持久性数据结构应用很多。


虽然持久性数据结构是对共享数据结构进行并发修改的理想解决方案,但是持久性数据结构往往无法很好地执行。


例如,一个持久列表会将所有新元素添加到列表的开头,并返回对新添加元素的引用(该引用随后指向列表的其余部分)。所有其他线程仍保留对列表中先前第一个元素的引用,并且对这些线程而言,列表保持不变。他们看不到新添加的元素。


这样的持久列表被实现为链表。不幸的是,链表在现代硬件上的表现不佳。链表中的每个元素都是一个单独的对象,这些对象可以分布在整个计算机的内存中。现代CPU顺序访问数据的速度要快得多,因此在现代硬件上,从头遍历的数组存储数据可以获得更高的性能。CPU高速缓存可以一次将更大的数据块加载到高速缓存中,并让CPU在加载后直接访问CPU高速缓存中的数据。对于链表,将元素分散在整个RAM上,这实际上是不可能的。


Stateless Workers


共享状态可以由系统中的其他线程修改。因此,工作人员必须在需要时重新读取该状态,以确保它在最新副本上正常工作。无论共享状态是保留在内存中还是在外部数据库中,都是如此。不在内部保持状态(但每次需要时都会重新读取状态)的工作程序称为无状态。


每次需要时重新读取数据都会变慢。特别是如果状态存储在外部数据库中。


Job Ordering is Nondeterministic

另外一个缺点是job的执行是无序的。没有办法来保证job之间执行的完成顺序。


Assembly Line




第二个高并发模型是Assembly Line流水线模型。它还可能有其他名字例如reactive systems,event driven systems。具体如下图:


深度·连载 | Java并发与多线程模型:并发模型


工人们被组织的就想工厂里的流水线一样。每个工人负责其中一步,完成之后给下一个。每个工作程序都在自己的线程中运行,并不与其他工作程序共享什么。所有有时也成它为无共享并发模型。


使用Assembly Line模型的系统通常使用非阻塞IO。非阻塞IO意味着当工作进程开始IO操作(例如从网络连接读取文件或数据)时,工人不会等待IO的结束。IO操作非常的缓慢,等待IO操作结束会浪费CPU大量时间,这个事件CPU可以做一些其他事。当IO执行完成之后,结果会返回给其他工人。


深度·连载 | Java并发与多线程模型:并发模型


实际上,作业可能不会沿着一条装配线流动。由于大多数系统可以执行一项以上的工作,因此工作会根据需要完成的工作在不同的工作人员之间流动。实际上,可能同时存在多个不同的虚拟装配线。这就是流水线系统中的工作流程实际的样子:


深度·连载 | Java并发与多线程模型:并发模型


甚至可以将作业转发给多个工人进行并行处理。例如,可以将作业转发给作业执行者和作业记录器。此图说明了三个装配线如何通过将其作业转发给同一工人(中间装配线中的最后一个工人)来完成:


深度·连载 | Java并发与多线程模型:并发模型


Reactive, Event Driven Systems


使用Assembly Line模型的系统有时也称为反应系统或事件驱动系统。系统的工作人员会对系统中发生的事件做出反应,这些事件是从外界接收到的,或者是其他工作人员发出的。事件例如:传入的HTTP请求,或者某个文件已完成加载到内存等。


有一些常见的事件驱动系统:

• Vert.x

• AKKa

• Netty


Actors vs. Channels


Actors 和 Channels是Assembly Line模型的两个典型应用。


在actor模型中每个工人都被称为actor。Actors可以给互相直接发送信息。消息的发布都是异步的。Actors可以用来实现一个或多个job流程的Assembly Line模型。如下图所示:


深度·连载 | Java并发与多线程模型:并发模型


在channel模型中,workers不能直接相互沟通。而是他们发布自己的信息在不同的channels中。其他工人可以监听这些channels中的信息,而发送者不需要知道谁在监听。如下图所示:


深度·连载 | Java并发与多线程模型:并发模型


channel模型相比较来说,是更灵活。工人只需要知道自己要发送什么信息到channel中。频道上的监听器可以订阅和取消订阅,而且不会影响工作人员写入channel。这使得工人们之间的联系变得不是那么紧。


Assembly Line优点




No Shared State


工人之间不共享任何数据,意味着不需要考虑并发访问共享状态可能引起的的所有并发问题。


Stateful Workers


因为工人知道没有其他线程修改其数据,因此工作人员可以是有状态的。有状态的意思是他们可以将需要操作的数据保留在内存中,仅将更改写回最终的外部存储系统。因此,有状态工人通常比无状态工人更快。


Better Hardware Conformity

单线程代码优势在于,它的工作模式与底层硬件更相符。


• 当你确定底层是以单线程模式运行的时候,通常可以创建更多的数据结构和算法。

• 单线程有状态的worker能够在缓存中操作数据,速度更快。


Job Ordering is Possible


job的执行可以有顺序执行。


Assembly Line 缺点




组装流水线并发模型的主要缺点是,作业的执行通常分散在多个工作人员中,因此也分散在项目中的多个类中。因此,很难确切地知道给定作业正在执行什么代码。


编写代码也可能会更困难。辅助代码有时被编写为回调处理程序。具有许多嵌套回调处理程序的代码可能会导致某些开发人员称之为回调地狱。回调地狱只是意味着很难跟踪所有回调中代码的实际作用,以及确保每个回调都可以访问所需的数据。


使用并行工作程序并发模型,这往往更容易。您可以打开工作程序代码,并从头到尾阅读几乎执行的代码。当然,并行工作程序代码也可以分布在许多不同的类上,但是执行顺序通常更容易从代码中读取




Which Concurrency Model Is Best



通常,答案是这取决于系统应该执行的操作。如果您的工作自然是并行的,独立的并且不需要共享状态,则可以使用parallel worker model来实现系统。


但是,许多工作并非自然而然地平行和独立。对于这些类型的系统,我相信Assembly Line的优点要大于缺点,比并行工作器模型要有更多的优点。


当然,这里只是讲了两种并发模型,还有更多的,例如Functional Parallelism,same-threading等等,在程序设计的时候,实现功能不难,但是保证可靠性,稳定性,我们需要考虑的更多,当然回归主题,没有最好的模型,只有最合适的模型。




作者介绍

张程,2017年毕业于西安建筑科技大学,现任职于北银金融科技有限责任公司大数据开发部。主要擅长金融大数据分析研发,JAVA"三高"架构落地与优化。


深度·连载 | Java并发与多线程模型:并发模型



招聘启事


北银金融科技有限责任公司根植于北京银行,是一家致力于大数据、人工智能、云计算、区块链、物联网等新技术创新与金融科技应用的科技企业,公司充分发挥北京银行企业文化和技术积淀先天优势,通过对技术、场景、生态的完美融合,输出科技创新产品和技术服务。


现诚邀优秀人才加盟,

共享金融科技时代硕果


扫描此二维码

期待您的加入