欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  Java

Java 并发模型的详细介绍

程序员文章站 2022-05-23 14:40:29
...
并发系统可以使用不同的并发模型去实现。一个并发模型指定着线程在系统协作中是如何完成被给与的任务。不同的并发模型使用不同的方式分解工作,以及线程之间可能用不同的方式沟通和协作。这个并发模型教程将会深入的讲解在我写的时候的使用的最广泛的并发模型。

并发模型和分布式系统类似

在这个文字中讲到的并发模型跟在分布式系统中使用的不同的框架是类似的。在一个并发系统中不同的线程之间互相沟通。在一个分布式系统中不同的进程进行交流(可能是不同的计算机)。本质上线程和进程是非常相似的。这个就是为什么不同的并发模型经常看起来跟不同的分布式框架相似的原因。

当然分布式系统也有额外的挑战,例如网络可能出现故障,或者远程计算机以及进程当掉等等。但是运行在一个大的服务器的并发系统中,如果一个CPU出现故障,网卡故障,磁盘故障等等也可能出现相似的问题。这种故障的可能性虽然可能比较低,但是理论上也有可能发生。

因为并发模型跟分布式系统框架是类似的,所以他们之间经常可以借鉴一些思想。例如,在工作者(线程)之间分配工作的模型跟在分布式系统中的负载均衡是类似的。处理错误的技术像日志,容错等等也是相同的。

并行工作者

第一个并发模型,我们称之为并行工作者模型。进来的任务分配给不同的工作者。这里有一个示意图:

Java 并发模型的详细介绍

在并行工作者并发模型中,一个代理分配进来的工作到不同的工作者。每一个工作者完成整个的任务。整个工作者是并行工作的,运行在不同的线程中,以及可能是在不同的CPU中。

如果一个并行工作者模型在一个汽车工厂中被实现,每一辆车将会被一个工作者生产。这个工作者将会得到说明书去建造,并且将要构建从开始到结束的每一件事情。

这个并行工作者并发模型是在java应用中使用的最广泛的并发模型(虽然那个正在改变)。在java.util.concurrent的Java包中的许多并发工具类被设计使用这个模型。你也可以在Java企业级应用中看到这个模型的痕迹。

并行工作者的优势

并行工作者并发模型的优势在于理解起来比较简单。为了增加应用的并行计算,你只是需要添加更多的工作者就可以了。

例如,你正在实现一个网页爬虫的功能,你将会使用不同数量的工作者去抓取一定数量的页面,以及看看哪个工作者将会花费更短的抓取时间(意味着更高的性能)。因为网页抓取是一个IO密集型工作,你可能是以计算机上的每个CPU/内核多个线程结束。一个CPU一个线程太少了,因为它在等待数据下载的时候将会空闲很长时间。

并行工作者的劣势

并行工作者并发模型有一些劣势潜伏在表面。我将会在下面的部分解释大部分的劣势。

共享状态获取复杂

在现实中并行工作者并发模型比上面说明的更加复杂。这个共享的工作者经常需要访问一些共享数据,或者在内存中或者在共享的数据库中。下面的图显示了并行工作者并发模型的复杂性。

Java 并发模型的详细介绍

这个共享状态的一些是处在像工作队列的通信机制里面。但是这个共享状态的一些是业务数据,数据缓存,数据库连接池等等。

只要共享状态悄悄的进入并行工作者并发模型,它就开始变得复杂了。这个线程在某种程度上需要访问共享的数据以确保被一个线程改变的对其他线程也是可见的(推向主内存,并且不只是陷入到执行这个线程的CPU的CPU缓存)。线程需要避免竞态条件,死锁,以及许多其他的共享状态并发问题。

此外,当访问共享的数据结构的时候,当线程之间互相等待,并行计算的部分丢失了。许多并发的数据结构正在堵塞,意味着一个或者一组有限的线程集合可以在任何给予的时间访问他们。这个就可能在这些共享的数据结构上导致竞争。高竞争将会本质上导致访问共享的数据结构代码部分执行的一定程度上的串行化。

现代的非堵塞的并发算法可能会降低竞争,以及提高性能,但是非堵塞算法很难被实现。

持久化的数据结构是另外一个可供选择的。一个持久化的数据结构当被修改的时候总是会保存它自己之前的版本。此外,如果多个线程指向相同的持久化数据结构,并且其中一个线程修改它,正在修改的这个线程得到一个新的结构的引用。所有其他的线程将会保持对老的结构的引用,这些老的结构仍然是没有改变的。这个Scala编程语言包含了几个持久化的数据结构。

当持久化的数据结构对于共享的数据结构的并发修改一个优雅的简练的解决方案的时候,持久化的数据结构不会执行的很好。

例如,一个持久化的列表将会添加所有新的元素到这个列表的头部,并且返回对于新添加元素的一个引用(这个然后执行这个列表的剩余部分)。所有其他的线程仍然保持一个对列表中前面第一个元素的引用,并且对于其他线程这个列表显示未改变的。他们不能看到这个新添加的元素。

这样的一个持久化列表作为一个链表实现。不幸的是链表在现代软件中不会执行的很好。在列表中的每一个元素都是分开的对象,以及这些对象可以被传播到所有的计算机内存中。当代CPU在顺序的访问数据会更快的,以至于在现代硬件上在一个数组的顶层不是列表实现将会得到一个更高的性能。一个数组顺序的存储数据。这个CPU缓存可以一次加载更大的块进入缓存,并且在这个CPU缓存一旦加载完就可以直接访问数据。这个如果用链表实现是不可能的,因为在链表里面的元素将会分散到所有的RAM上。

无状态的工作者

在系统*享的状态可以被其他的线程修改。因此工作者每次需要它的时候都要重新读取这个状态,去确认是否工作在最新的拷贝上。这是真的,不管这个共享状态是在内存中还是在外部的数据库中。一个在内部没有保持状态的工作者称之为无状态的(但是需要每次重新读取)。

每次都需要重新读数据会变慢的。尤其是如果这个状态存储在外部的数据库中。

任务顺序是不能确定的

另外一个并行工作者模型的劣势就是执行任务的顺序是不能确定的。这里没有办法保证那个任务先执行,那个任务最后执行。任务A可能会在任务B之前给予一个工作者,然而任务B可能在任务A之前执行。

并行工作者模型自然地不确定使得很难去推论在某个时间点上系统的状态。它也会很难去保证一个任务在另外一个任务之前发生(基本上是不可能的)。

流水线(Assembly Line)

第二个并发模型,我称之为流水线并发模型。我选择这个名字只是为了适合“并行工作者”比喻的更简单。其他的开发者使用其他的名字(例如,反应系统,或者事件驱动系统)依赖平台或者社区。下面有个示例图进行说明:

Java 并发模型的详细介绍

这个工作者就像是一个工厂里的流水线上的工人一样。每一个工人只是执行全部工作的一部分。当那个部分完成之后,这个工人就会把这个任务转给下一个工人。

每一个工作者都是运行在他们自己的线程里面,并且工作者之间没有共享的状态。所以这个有时候也会作为一个无共享的并发模型被提及。

使用流水线并发模型的系统通常会使用非堵塞IO来设计。非堵塞IO意味着当一个工作者开始一个IO操作的时候(例如读文件或者来自网络连接的数据),这个工作者不会等到IO调用结束。IO操作是慢的,以至于等待IO操作完成是浪费CPU的。这个CPU可以同时做一些其他的事情。当这个IO操作结束的时候,这个IO操作的结果(例如数据读取或者数据写的状态)会传递给另外一个工作者。

对于非堵塞IO,这个IO操作决定着工作者之间的边界范围。一个工作者做他能做的直到它不得不开始一个IO操作。然后它放弃控制这个任务。当这个IO操作结束的时候,这个流水线上的下一个工作者继续在这个任务上工作,直到那个也不得不开始一个IO操作等等。

Java 并发模型的详细介绍

实际上,这些任务不一定流动在一个生产线上。因为大部分的系统不只是执行一个任务,工人与工人之间的任务流动依赖于需要被做的那个任务。实际上,这里将会有多个不同的虚拟流水线同时在进行。下面的图示就是真正的流水线系统中的任务是如何流动的。

Java 并发模型的详细介绍

任务甚至为了并发运行可能会执行不止一个工作者。例如,一个工作可能同时指向一个任务执行者和一个任务日志。这个图示说明了所有的三个流水线是怎样通过把他们的任务指向相同的工作者而结束的(最后的工作者在中间的流水线上):

Java 并发模型的详细介绍

流水线甚至可以得到的比这个更加复杂的。

反应系统,事件驱动系统

使用一个流水线并发模型的系统通常也被称之为反应系统,事件驱动系统。这个系统的工作者对系统中正在发生的事件作出反应,或者从外部接受或者被其他的工作者发出。事件的例子可以是一个HTTP请求,或者是某一个文件结束加载到内存等等。

在写的时候,这里有许多有趣的反应/事件驱动平台可用,并且在将来会有更多的。更普遍的一些看起来如下:


  • Vert.x

  • Akka

  • Node.JS(JavaScript)

对于我个人,我发现Vert.x更加有趣(尤其是想我对于Java/JVM过时的人)

作用者 VS. 通道

作用者和通道是流水线(或者是反应系统/事件驱动)模型中两个相似的例子。

在作用者模型中每一个工作者都称之为作用者。作用者可以互相彼此发送消息。消息被发送,然后异步的执行。作用者可以使用去实现一个或者更多的任务,正如之前描述的。这里有一个作用者的模型:

Java 并发模型的详细介绍

在通道模型中,工作者不能互相之间进行通信。代替的,他们发布他们的信息(事件)在不同的通道中。其他的工作者可以监听这些通道的信息,然而发送者不会知道谁正在监听。这里有一个通道模型的图示:

Java 并发模型的详细介绍

在写的时候,这个通道模型对我来说看起来更有弹性。一个工作者不需要知道在流水线上后来工作者要执行什么任务。它只需要知道通道将会指向这个工作什么(或者发送信息到哪里)。在通道上的监听者可以订阅或者取消,不会影响正在写通道的工作者。这个就允许在工作者之间松散耦合。

流水线的优势

这个流水线并发模型相对于并行工作者模型来说有几个优势。在下面的部分我将会覆盖到最大的优势。

没有共享状态

事实上,工作者不会跟其他的工作者分享状态,意味着他们不用考虑所有的并发问题去实现。这个就使得它去实现工作者更加简单。你实现一个工作者当如果只是一个线程执行那个工作,本质上就是一个单线程实现。

有状态的工作者

因为工作者知道不会有其他的线程修改他们的数据,所以这个工作者是有状态的。有状态的,意味着他们可以保持需要在内存中操作的数据,只是把最终的改变写回到外部的存储系统中。一个有状态的工作者因此比一个无状态的工作者更快。

更好的硬件整合

单线程的代码有这个优势,它经常跟底层的硬件适配的更好。首先,当你假设代码可以在单线程模式中执行的时候,你可以创建更多优化的数据结构和算法。

第二,单线程有状态的工作者正如上面提到的,可以缓存内存中的数据。当数据被缓存到内存的时候,这里也有很高的可能性,数据也会缓存在执行线程的CPU的CPU缓存中。这个就使得访问数据更快。

我指的它作为硬件整合当代码被写的时候,从某种程度上得益于底层硬件的工作方式。一些开发者称之为硬件的运作方式。我更喜欢硬件整合这个术语,因为计算机有很少的机械部分,并且这个词“sympathy”在本文中为了“适配更好”作为一个暗喻来使用的,然而我相信这个词“conform”表达的更加合理。

不管怎么样,这个是吹毛求疵的。使用你喜欢的词就可以了。

有顺序的任务是可能的

根据流水线并发模型实现一个并发系统从某种程度上保证任务的顺序是可能的。有顺序的任务使得在任何给予的时间点去推理系统的状态更加简单。而且,你可以写所有进来的任务到一个日志。这个日志可以被用来一旦系统的部分失败,可以从失败的地方重建。这个任务可以用一个确定顺序写入到日志,并且这个顺序变成固定的任务顺序。设计的图示如下所示:

Java 并发模型的详细介绍

实现一个固定的任务顺序当然不是简单的,但是它是可能的。如果可以,它会很大的简化像备份,恢复数据,复制数据等等这样的任务。这些都可以通过日志文件去做。

流水线的劣势

流水线并发模型的主要劣势就是任务的执行经常会传播到多个工作者,并且会通过你项目中的多个类。因此对于去确切的看给予的任务正在执行什么代码将会变的更加困难。

写代码可能也会变的困难。工作者代码经常会作为回调函数来写。伴随着更多的嵌套的回调函数的代码可能会导致一些开发者调用什么回调函数厌烦。回调地狱只是意味着更加困难的去跟踪代码正在做的事情,和确定每一个回调需要访问他们的数据一样。

使用并行工作者并发模型,这个将会变的更加简单。你可以打开这个工作者代码,并且几乎可以从开始到结束读取已经执行的代码。当然,并行工作者代码可能也会扩散到不同的类中,但是执行的序列进场会更简单的从代码中读取。

功能并行性(Functional Parallelism)

功能并行性是第三个并发模型,这个模型在这些年被讨论的非常多。

功能并行性的基本思想是使用函数调用实现你的程序。函数被看为“代理”或者“扮演者”去互相发送信息,就像流水线并发模型(AKA反应系统或者事件驱动系统)。当一个函数调用另外一个的时候,那个是跟发送一个信息是相似的。

传递给函数的所有参数是被拷贝的,以至于正在接收的函数的外部没有实体可以组合这个数据。这个拷贝对于避免共享数据的静态条件是至关重要的。这个使得这个函数执行跟一个原子的操作是类似的。每一个函数调用都能跟任何其他的函数调用单独的执行。

当一个函数调用可以被单独执行的时候,每一个函数调用都可以在分离的CPU上执行。那就意味着,一个被实现的功能算法就可以并行的执行,在多个CPU上。

使用Java 7,我们得到java.util.concurrent包包含ForkAndJoinPool,这个可以帮助你实现跟功能并行性类似的事情。使用Java 8,我们得到一个并行streams,这个帮助你并行化大的集合的迭代。记住,有写开发者对ForkAndJoinPool感到不满的(在我的ForkAndJoinPool教程里面你可以发现一些批评的链接)。

功能并行性最困难的部分就是去知道哪个函数调用去并行化。跨越CPU协作配合的函数调用带来了一个开销。被一个函数完成的工作单元需要某个大小的开销。如果这个函数调用非常小,试着去并行化他们可能确实会比一个单线程执行,单独的CPU执行会慢。

来自我的理解(这个当然并不完美),你可以使用一个反应系统,时间驱动去实现一个算法,以及去完成一个工作的分解,这个就是跟功能并行性类似的。伴随着一个事件驱动模型,你只是得到一个确定更多的控制多少以及怎么样去并行化(我认为)。

另外,在多个CPU之上伴随着协调配合的开销分解一个任务,如果那个任务当前只是被这个程序执行的唯一任务才会有意义。然而,如果系统正在执行多个其他的任务(例如,web服务器,数据库服务器以及许多其他的系统),那么这里就没有意义去并行化一个单独的任务。在计算机上的其他CPU不管怎么都会忙着执行其他的任务,以至于没有理由去使用一个更慢的功能并行的任务去打乱他们。你最可能明智的去使用一个流水线并发模型,因为它有更小的开销(在单线程模式下顺序的执行),并且跟底层的硬件有更好的符合要求。

哪一个并发模型是最好的

那么,哪一个并发模型是最好的呢?

通常就是这样,这个答案取决于你的系统将会是什么样子的。如果你的任务是自然的并行的,独立的,以及不需要有共享状态的,那么你可能会使用并行工作模型去实现你的系统。

许多任务尽管不是自然并行的和独立的。对于这些种类的系统,我相信这个流水线并发模型相对劣势来说有更多的优势,并且比并行工作者模型有更大的优势。

你甚至不需要自己去写流水线结构的代码。现代的平台像Vert.x已经为你实现很多了。我个人将会在下一个项目探索运行在像Vert.x平台顶层之上的设计。我感觉,Java EE不会有任何优势了。

以上就是Java 并发模型的详细介绍的内容,更多相关内容请关注PHP中文网(www.php.cn)!