Java 并发/多线程教程(四)-并发模型

       本系列译自jakob jenkov的Java并发多线程教程(本章节部分内容参考http://ifeve.com/并发编程模型),个人觉得很有收获。由于个人水平有限,不对之处还望矫正!

       并发系统可以有多种并发模型来实现,并发模型指定线程如何协同完成分配给他们的任务。不同的并发模型以不同的方式划分任务,并且线程与线程之间以不同的方式进行通信和协作。

并发模型与分布式系统的相似性

         本文中描述的并发模型类似于分布式系统中使用的不同体系结构。在并发系统中,不同的线程彼此通信。在分布式系统中,不同的进程彼此通信(可能在不同的计算机上)。线程和进程在本质上是非常相似的。这就是为什么不同的并发模型通常看起来与不同的分布式系统体系结构相似。

        分布式系统在处理网络失效、远程主机或进程宕掉等方面也面临着额外的挑战。但一个并发系统运行在一个大型服务器中有可能也会遇到类似的问题,比如一块 CPU 失效、一块网卡失效或一个磁盘损坏等情况。虽然失败的概率可能较低,但在理论上它仍然可能发生。

        由于并发模型与分布式系统体系结构相似,所以它们常常可以互相借鉴。例如,为工作者们(线程)分配作业的模型一般与分布式系统中的负载均衡系统比较相似。同样,它们在日志记录、失效转移、幂等性等错误处理技术上也具有相似性。

1、并行工作者(Parallel worker)

在并行worker模型中,传入的作业会被分配到不同的工作者,下图展示并行工作者模型

图片发自简书App

在并行工作者模型中,委派者将传入的作业分配给不同的工作者。每个工作者完成整个任务,这些工作者并行运行在不同的线程上,甚至可能不在同一个CPU上。

       如果在车厂利用并行工作者模型,那么每一辆车都是由一工人来完成的,这些工人都要有汽车的生产说明书,并且从头到尾完成一辆车的生产。

       并行工作者并发模型是Java应用中最常用的一种并发模型(尽管这个情况在改变)。在java.util.concurrent包中的很多并发工具类的目的是为了应用这个模型。在Java企业版的服务器应用中,也可以看到这个模型的踪迹。

并行工作者模型的优点

      并行工作者模型的优点是他容易理解,如果想要增加应用的并发规模,你只需要添加更多的工作者即可。

      例如:你正在实现一个网络爬虫,你可以通过多个工作者来爬一定数量的页面,然后看在一定量的页面情况下,调整工作者个数,看多少个工作者用时最短(意味着高性能),由于网络爬虫是一种密集型IO任务,所以最终结果很可能是一个CPU中可以运行多个爬虫线程。这种情况下,如果一个CPU只有一个爬虫线程将会浪费CPU资源,因为下载数据通常会产生大量的CPU等待时间的。

并行工作者模型的缺点

      并行工作的并发模型在其简单的外表下有一些隐藏缺点。我将在下面的内容中阐述几个最明显的缺点。

A)共享状态带来了复杂性

     在实际的的运用中,并行工作者模型远比上面的例子复杂。共享工作者需要访问一些共享数据,主些共享数据可能是内存数据或是数据库数据。下面的图来展示这样的情形如何使得并行工作者变得更为复杂:

       有时,这些共享状态可能是通讯机制里的工作队列,有时可能是业务数据,缓存数据,数据库连接池等。

       一旦并行工作者模型中有共享状态,那么将会变得很复杂,线程访问共享数据,需要保证共享数据如果被一个线程更改,要对其他的线程可见(将修改结果同步到主存中,而不是当前执行线程的CPU缓存中)。线程间需要避免竞争、死锁和其他的一些因共享状态并发问题。

       除此之外,部分并行线程在等待访问共享状态时,许多并发数据结构被阻塞,这就意味着在同一时刻限制了其他线程对他们的访问。这将导致线程对这些共享数据结构的竞争,从本质上讲,高竞争状态会导致获取共享数据的代码在一定程度上串行化运行。

       现行非阻塞式的并行算法有可能会减少竞争和提高性能,但是非阻塞式的并行算法很难实现 。

       持久化数据是另外一种选择,待久化数据结构在修改之前通常会保存上一个版本的数据,因些,如果多个线同时访问持久化数据时,如果其中一个线程对数据进行修改,修改的线程获取这个数据结果的一个新的引用,而其他线程则保持原有数据结构的引用。Scala语言具有几种持久化的数据结构。

      虽然持久化数据结构是共享数据并发读写中遇到的问题的一个看似“优雅的”解决方案,但其性能并不那么好。

     例如:一个待久化的列表,添加一些新的元素到这个列表的头部,然后返回新加元素的引用。其他的线程仍旧保持持久化列表之前的引用,因此对这些线程来说,新添加的元素对他们是不可见的。

B)无状态的工作者

      由于共享状态可能被系统中的其他线程修改。所以工作者在需要用到的时候必须重新去读取这个状态,以确保当前工作者运行在最新的副本上。不管这个状态是保存于内存中,还是保存在外部的数据库中,都是这样去实现的。一个工作者,不需要在线程内部保存共享数据的状态,而是在需要用到的时候再次重新去获取共享数据的状态,这样就称之为无状态。

      每次重新去获取共享数据的状态会变慢,尤其是共享状态存储在外部的数据库中。

C) 工作顺序是不确定的

      并行工作者的另一个缺点就是工作顺序的不确定。我们没有办法去保证任务按顺序执行,例如:任务A有可能在任务B之前执行,也有可能任务B先于任务A执行。

      并于工作者模型的顺序不确定性使得我们很难确定在给定的某个时间的状态。并且也很难保证一个任务发生于另一个任务之前 。

2、流水线模型

        第二种并发模型称之为流水线模型。我之所以取这个名字,只是为了配合“并行工作模型”。其他的程序员,可能会用其他名字。下图表示一个流水线模型。

       像工作生产线上的工人们一样组织工作者,每个工作者只负责整个任务中的一部分,当完成自己这部分的工作的工作者会将任务转发给下一个工作者。每个工作者都在自己的线程中运行,并且不会和其他 的工作者共享状态。因此有时也称之为无共享并行模型。

       通常使用非阻塞的IO来设计使用流水线并发模型的系统。非阻塞意味着当一个工作者操作IO时(比如:读取文件或是从网络连接中获取数据),工作者不需要等到IO操作结束,由于IO操作非常慢,因此等待IO操作完成很浪费CPU时间,在等待IO操作完成的同时,CPU可以做些其他的事情,当IO操作完成时,被传递给下一个工作者。

       有了非阻塞IO,就可以使用IO操作来确定工作者之间的边界。工作者会尽可能多的运行直到遇到并启动一个IO操作。然后交出工作的控制权,当IO操作完成后。然后在流水线上的下一个工作者继续进行操作。直到他也遇到并启动一个IO操作。

      实事上,作业不可能沿着单一的流水线,由于多数的系统可能执行多个作业。作业从一个工作者流向另一个工作者取决于作业需要做的工作。在实际中可能会有多个不同的虚拟流水线同时运行。下面是现实中作业在流水线系统中可能的情况:

       作业可能被转发到多个工作者上并发处理。比如:作业可能同时被转发到任务执行器和作业日志。下图说明了三条流水线是如何把一个作业转发给同一个工作者(中间流水线的最后一个工作者)来完成作业:

       流水线作业有时会比这个更复杂

响应(Reactive),事件驱动

     采用流水线并发模型的系统有时也称之为Reactive或是事件驱动系统。系统中的工作者会对来自系统内部的事件,或者接受外部的请求或者其他工作传入的事件做出响应。举个例子:事件有可能是来自于外部的HTTP请求,也有可能是某个文件成功加载到内存中。

Actors vs. Channels

      Actors和Channels是两种比较类似的流水线(Reactive/事件驱动)模型。在Actor模型中每个工作者被称之为actor,Actor之间可以异步发送和处理消息。actor可以被用来实现一个或多个流水线作业。下图给出了actor模型的:

      而在channel模型中。工作者之间不直接与其他工作者通信。而是他们把他们的消息(事件)发布到另一个不同的channel,其他工作者可以监听在这个channel上,而发布者不需要知道是谁在监听。下图给出了channel模型:

     在写这篇文章的时候,channel模型对于我来说似乎更加灵活。一个工作者无需知道谁在后面的流水线上处理作业。只需知道作业(或消息等)需要转发给哪个通道。通道上的监听者可以随意订阅或者取消订阅,并不会影响向这个通道发送消息的工作者。这使得工作者之间具有松散的耦合。

流水线模型的优点

      相对于并行工作者模型,流水线模型具有几个优点,在接下来的章节中我会介绍几个最大的优点。

无共享状态

      工作者之间无状态。意味着实现的时候无需考虑所有因并发访问共享对象而产生的并发性问题。这使得在实现工作者的时候变得非常容易。在实现工作的时候就好像是单个线程在处理工作一样,这基本上是一个单线程的实现 。

有状态的工作者

        当工作者知道没有别的线程修改它们的数据,工作者可以是有状态的,对于有状态,我的意思是,它们可以在内存中保存要操作的数据,只需要在最后把更改的结果写回到外部存储系统。因此,有状态的工作者通常比无状态的工作者更快。

更好的与硬件结合

        单线程代码通常在结合底层硬件时具有更好的优势。首先,当你假设代码只在单线程模型下运行时,通常能能创建更优的数据结构和算法 。

      其次,单线程的工作者可以缓存数据到内存中,当数据缓存在内存中,有很大的可能数据也缓存在执行这个线程的CPU中,这使得访问缓存数据变得很快。

合理的作业顺序

      基于流水线并发模型实现的并发系统,在某种程度上是有可能保证作业的顺序的。作业的有序性使得它更容易地推出系统在某个特定时间点的状态。更进一步,你可以将所有到达的作业写入到日志中去。一旦这个系统的某一部分挂掉了,该日志就可以用来重头开始重建系统当时的状态。按照特定的顺序将作业写入日志,并按这个顺序作为有保障的作业顺序。下图展示了一种可能的设计:

      实现一个有保障的作业顺序是不容易的,但往往是可行的。如果可以,它 将大大简化一些任务,例如备份、数据恢复、数据复制等,这些都可以通过日志文件来完成。

流水线模型的缺点

      流水线并发模型最大的缺点是作业的执行往往分布到多个工作者上,并因此分布到项目中的多个类上。这样导致在追踪某个作业到底被什么代码执行时变得困难。

       流水线并发模型最大的缺点是作业的执行往往分布到多个工作者上,并因此分布到项目中的多个类上。这样导致在追踪某个作业到底被什么代码执行时变得困难。

      使用并行工作者模型可以简化这个问题。你可以打开工作者的代码,从头到尾优美的阅读被执行的代码。当然并行工作者模式的代码也可能同样分布在不同的类中,但往往也能够很容易的从代码中分析执行的顺序。

3、函数式并行(Functional Parallelism)

      第三种并发模型是函数式并行模型。函数式并行的基本思想是采用函数调用实现程序。函数可以看作是”代理人(agents)“或者”actor“,函数之间可以像流水线模型(AKA 反应器或者事件驱动系统)那样互相发送消息。某个函数调用另一个函数,这个过程类似于消息发送。

      函数都是通过拷贝来传递参数的,所以除了接收函数外没有实体可以操作数据。这对于避免共享数据的竞态来说是很有必要的。同样也使得函数的执行类似于原子操作。每个函数调用的执行独立于任何其他函数的调用。

       一旦每个函数调用都可以独立的执行,它们就可以分散在不同的CPU上执行了。这也就意味着能够在多处理器上并行的执行使用函数式实现的算法。

       Java7中的java.util.concurrent包里包含的ForkAndJoinPool能够帮助我们实现类似于函数式并行的一些东西。而Java8中并行streams能够用来帮助我们并行的迭代大型集合。

      函数式并行里面最难的是确定需要并行的那个函数调用。跨CPU协调函数调用需要一定的开销。某个函数完成的工作单元需要达到某个大小以弥补这个开销。如果函数调用作用非常小,将它并行化可能比单线程、单CPU执行还慢。

       我个人认为(可能不太正确),你可以使用响应模型(Reactive)或者事件驱动模型实现一个算法,像函数式并行那样的方法实现工作的分解。使用事件驱动模型可以更精确的控制如何实现并行化(我的观点)。

       此外,将任务拆分给多个CPU时协调造成的开销,仅仅在该任务是程序当前执行的唯一任务时才有意义。但是,如果当前系统正在执行多个其他的任务时(比如web服务器,数据库服务器或者很多其他类似的系统),将单个任务进行并行化是没有意义的。不管怎样计算机中的其他CPU们都在忙于处理其他任务,没有理由用一个慢的、函数式并行的任务去扰乱它们。使用流水线(反应器)并发模型可能会更好一点,因为它开销更小(在单线程模式下顺序执行)同时能更好的与底层硬件整合。

使用那种并发模型最好?

       所以,用哪种并发模型更好呢?

       通常情况下,这个答案取决于你的系统打算做什么。如果你的作业本身就是并行的、独立的并且没有必要共享状态,你可能会使用并行工作者模型去实现你的系统。虽然许多作业都不是自然并行和独立的。对于这种类型的系统,我相信使用流水线并发模型能够更好的发挥它的优势,而且比并行工作者模型更有优势。

       你甚至不用亲自编写所有流水线模型的基础结构。像Vert.x这种现代化的平台已经为你实现了很多。我也会去为探索如何设计我的下一个项目,使它运行在像Vert.x这样的优秀平台上。

时间: 2024-11-03 12:04:57

Java 并发/多线程教程(四)-并发模型的相关文章

Java 并发/多线程教程(六)-并发VS并行

       本系列译自jakob jenkov的Java并发多线程教程,个人觉得很有收获.由于个人水平有限,不对之处还望矫正!         在多线程线程编程中,我们经常提及并发和并行,但是并发和并行究竟是什么意思,他们所要表达是同一回事,还是不是同一加事.        它们当然不是一回事,虽然表面上它们看起来非常的相似,但是它们是两个不相同的术语.在此之前,我也花费了大量的时间去了解并发与并行的区别,因此我觉得在这里对比一下两者. 并发         并发意味着一个应用在同一时刻处理多

Java 并发/多线程教程(十一)-JAVA内存模型

本系列译自jakob jenkov的Java并发多线程教程,个人觉得很有收获.由于个人水平有限,不对之处还望矫正!         Java内存模型指定Java虚拟机如何与计算机的内存(RAM)一起工作.Java虚拟机是整个计算机的一个模型,所以这个模型自然包含了一个内存模型--也就是Java内存模型.         如果您想要设计正确的并发程序,那么理解Java内存模型是非常重要的.Java内存模型指定了不同线程如何以及何时可以看到由其他线程写入共享变量的值,以及在必要时如何同步访问共享变量

Java 并发/多线程教程(五)-相同线程

       本系列译自jakob jenkov的Java并发多线程教程,个人觉得很有收获.由于个人水平有限,不对之处还望矫正!        相同线程是一并发框架模型,是一个单线程系统向外扩展成多个单线程的系统.这样的结果就是多个单线程并行运行. 为什么是单线程系统?         你也许会感到好奇,为什么当今还有人设计单线程系统.单线程系统之所以这么普及,是因为单线程系统相对于多线程并发系统更为简单.单线程系统不需要与其他线程共享任何数据.这就使得单线程系统可以使用非并发的数据结构,可以更

Java 并发/多线程教程(十二)-JAVA同步块

本系列译自jakob jenkov的Java并发多线程教程,个人觉得很有收获.由于个人水平有限,不对之处还望矫正! 一个Java同步块标记一个方法或一个代码块作为同步.可以使用Java同步块来避免竞态条件. java同步关键字       在Java中同步的块被标记为Synchronized关键字.Java中的同步块在某些对象上是同步的.在同一对象上同步的所有同步块只能在同一时间内执行一个线程.所有试图进入同步块的其他线程都被阻塞,直到同步块中的线程退出该块. Synchronized关键字可以

Java 并发/多线程教程(八)-竞态条件和临界区

      本系列译自jakob jenkov的Java并发多线程教程,个人觉得很有收获.由于个人水平有限,不对之处还望矫正!       竞态条件是在临界区内可能发生的一种特殊情况.临界区是多线程并发执行一代码,根据线程的执行顺序可能产生多种结果的区域.多线程在临界区执行代码的结果可能不一样,不同的结果取决于线程的执行顺序.也就是说,临界区包含竞态条件.竞态一词源于隐喻,线程在临界区进进行资源竞争,在临界区的资源竞争影响最后的结果.      这听起来有点复杂,在下面的篇幅中,将会在下面的篇幅

Java 并发/多线程教程(七)-创建和启动java线程

      本系列译自jakob jenkov的Java并发多线程教程,个人觉得很有收获.由于个人水平有限,不对之处还望矫正! 创建和启动线程 在java中创建一个线程如下: Thread thread = new Thread(); 调用方法start()来启动一个线程: thread.start();         这个例子没有指定线程执行任何代码,线程将会在启动之后停止.         有两种方式指定线程应该执行什么代码.第一种方式就是创建一个Thread的子类并覆写run()方法.第

Java 并发/多线程教程(十)-线程安全及不可变性

本系列译自jakob jenkov的Java并发多线程教程,个人觉得很有收获.由于个人水平有限,不对之处还望矫正!        只有在多个线程访问相同的资源时,才会出现竞态条件,并且一个或多个线对相同的资源进操作.如果多个线程读取相同的资源条件,就不会发生这种情况.        我们通过使共享变量不可以变来确保共享变量不被别的线程修改,因此这样的共享变量是线程安全的,下面有个例子: public class ImmutableValue{       private int value =

Java 并发/多线程教程(九)-线程安全和共享资源

         本系列译自jakob jenkov的Java并发多线程教程,个人觉得很有收获.由于个人水平有限,不对之处还望矫正!       代码被多个线程同时调用是安全的,那么就称之为线程安全.如果一段代码是线程安全的,那么它没有竞态条件.竞态条件只有发生在多个线程更新共享资源.因些,清楚的知道线程执行时什么资源是共享的非常重要. 本地变量        本地变量存储在每个线程自己的栈里,这就意味着本地变量从不与其他线程共享.也就是说本地变量是线程安全的,下面是关于线程安全的本地变量的一个

Java 并发/多线程教程(三)-多线程的开销

        本系列译自jakob jenkov的Java并发多线程教程,个人觉得很有收获.由于个人水平有限,不对之处还望矫正!     应用程序由单线程到多线程,不仅仅给我带来了便利,同时也也带来了一些开销.不要因为你会多线程,就把所有的程序都设计成多线程.如果把单线程改成多线程,你获得到的好处要远远超过开销,对于这一点你应该有个清醒的认识.当你犹豫是应该用多线程还是单线程时,你要衡量性能和响应时间,而不是靠猜测. 更复杂的设计 尽管多线程应用程序的某些部分比单线程应用程序更简单,但其他部分