Java并发之actor模型

Actor模型是一个概念模型,用于处理并发计算。它定义了一系列系统组件应该如何动作和交互的通用规则。

线程-对象模型的问题

锁和效率

OOP的核心特性是封装,封装规定对象的内部数据不能直接从外部访问,它只能通过调用一组方法来修改。当我们分析OOP运行时行为时,我们有时会绘制一个消息序列图,显示方法调用的交互。例如:

很显然,对应单线程来说,程序就像串行一样执行,下面考虑多线程情况:

显然,在一个执行部分中,两个线程进入相同的方法。对象的封装模型并不保证该部分中的线程安全,解决此问题的常用方法是围绕这些方法添加锁定,确保了在任何给定时间最多只有一个线程将进入该方法,但这显然是一个非常昂贵的策略:

锁严重限制了并发性,在现代CPU架构上开销巨大。

调用者线程现在被阻塞,无法执行任何其他工作。

Locks引入了一个新的威胁:死锁。

这些现实导致了一个两难的局面:

没有足够的锁,状态就会被破坏。

由于存在许多锁定,性能会受到影响并且很容易导致死锁。

此外,锁只能在本地很好地工作。在协调跨多台机器时,唯一的选择是分布式锁。但是分布式锁的效率比本地锁要低得多。分布式锁定协议需要在多台计算机上通过网络进行多次通信往返,因此导致延迟。

我们经常将系统设想为对象实例的网络,它们对方法调用作出反应,修改其内部状态,然后通过方法调用相互通信,在多线程分布式环境中,实际是线程通过方法调用“遍历”对象实例网络。

总结:

对象只能在单线程访问时保护内部状态,多线程时,总是会导致内部转态损坏。

为了处理多线程问题必须要引入锁,但是实际上,锁的效率低下,且有死锁的风险性。

分布式锁的效率更低。

共享内存

80-90年代的编程模型表示,写入变量意味着直接写入存储位置(局部变量可能仅存在于寄存器中).在现代架构上,CPU正在写入缓存行而不是直接写入内存。这些高速缓存中的大多数都是CPU内核的本地高速缓存,也就是说,一个内核的写入不会被另一个内核看到。为了使本地更改对另一个核心可见,从而对另一个线程可见,需要将缓存行传送到另一个核心的缓存。

在JVM上,我们必须使用volatile标记或Atomic包装器明确表示要跨线程共享的内存位置。否则,我们只能在锁内部访问它们。为什么我们不将所有变量标记为volatile?因为跨核心运送缓存行是一项非常昂贵的操作!这样做会隐含地使所涉及的核心停止执行额外的工作,并导致高速缓存一致性协议(协议CPU用于在主存储器和其他CPU之间传输高速缓存线)的瓶颈。

总结:

现代架构中不再有真正的共享内存,cpu核心彼此间显式地传递数据。跨CPU核心通信已成为趋势。

除了使用易失性标记或是原子变量,更普遍的方法是将状态保存在并发实体本地,通过消息在并发实体之间传播数据。

回调

如何通知”调用者”完成任务?当任务因异常而失败时会出现更严重的问题。异常传播到哪里?它将传播到工作线程的异常处理程序,完全忽略了实际的”调用者”是谁。这种类似于网络系统的工作方式,其中消息/请求可能会丢失/失败而没有任何通知。

总结:

为了在当前系统上实现任何有意义的并发性和性能,线程必须以有效的方式在彼此之间委派任务而不会阻塞。使用这种类型的任务委派并发,基于调用堆栈的错误处理会崩溃,需要引入新的显式错误信令机制。使失败成为领域模型的一部分。

具有工作委派的并发系统需要处理服务故障,并具有从故障中恢复的原则方法。此类服务的客户端需要注意,任务/消息可能会在重新启动期间丢失。即使没有发生损失,由于先前排队的任务(较长的队列),垃圾回收所导致的延迟等,响应可能会被任意延迟。面对这些情况,并发系统应以超时的形式处理响应,例如联网/分布式系统。

actor system

下面来关注下actor模型如何处理这些问题。

使用消息来避免锁和阻塞

actor之间不是互相调用方法,而是彼此发送消息。发送消息不会使得执行线程从发送者转移到接收者。actor可以发送消息并继续进行而不会受到阻碍。因此,它可以在相同的时间内完成更多工作。

对于对象,当方法返回时,它释放对其执行线程的控制。在这方面,参与者的行为很像对象,它们对消息做出反应并在完成对当前消息的处理后返回执行。通过这种方式,参与者实际上实现了我们为对象设想的执行:

传递消息和调用方法之间的重要区别是消息没有返回值。通过发送消息,actor将工作委托给另一位actor。

我们在模型中需要进行的第二个关键更改是恢复封装。参与者对消息做出反应,就像对象“响应”对其上调用的方法一样。区别在于,actor独立于消息发件人执行,一次对一个传入消息进行响应,而不是多个线程“突入”我们的对象并破坏内部状态。当每个actor都按顺序处理发送给它的消息时,不同的参与者会同时并发工作,以便参与者系统可以同时处理尽可能多的消息。

由于每个actor最多只能处理一条消息,因此可以保持状态的不变性而无需同步。从而无需使用锁。

当actor收到消息时,会发生以下情况:

actor将消息添加到队列的末尾。

如果调度器未启动actor,则将其标记为准备执行。

调度器接受该actor并开始执行它。

Actor从队列的前面选择消息。

Actor修改内部状态,将消息发送给其他Actor。

actor解除调度。

为实现上述行为,actor具有:

邮箱(消息队列)。

行为(演员的状态,内部变量等)。

消息(代表信号的数据片段,类似于方法调用及其参数)。

执行环境(一种使收到消息的参与者响应并调用其消息处理代码的机制)。

地址。

上述模型解决了前面列举的问题:

通过将执行与信号分离(方法调用转移执行,消息传递不这样做)来保留封装。

不需要锁。只能通过消息修改actor的内部状态,消息一次处理一次,以保持不变性。

在任何地方都没有使用锁,发送者也不会被阻止。可以在十几个线程上有效地调度数百万个参与者,从而充分发挥现代CPU的潜力。任务委派是参与者的自然操作方式。

参与者的状态是本地的,而不是共享的,更改和数据通过消息传播,这些消息映射到现代内存层次结构的实际工作方式。在许多情况下,这意味着仅传递包含消息中数据的高速缓存行,同时将本地状态和数据保留在原始核心中。相同的模型准确地映射到远程通信,其中状态保存在机器的RAM中,更改/数据作为数据包在网络上传播.

优雅处理错误

由于不存在向彼此发送消息的参与者之间的共享调用堆栈,因此我们需要以不同的方式处理错误情况。我们需要考虑两种错误:

第一种情况是目标actor上的委派任务由于任务中的错误而失败(例如一些验证问题)。在这种情况下,目标actor的服务是完整的,只是任务本身是错误的。服务actor应该向发送方回复一条消息,说明错误情况。

第二种情况是服务本身遇到内部故障。Akka强制将所有actor组织成树状层次结构,这与操作系统将进程组织到树中的方式非常相似。就像流程一样,当一个actor失败时,其父actor可以决定如何对失败做出反应。同样,如果父actor停止,则其所有子代也将递归停止。这项服务称为监督,它是Akka的核心。

调度

Actor模型的任务调度方式分为“基于线程(thread-based)的调度”以及“基于事件(event-based)的调度”两种。

基于线程的调度为每个Actor分配一个线程,在接受一个消息时,如果当前Actor的“邮箱(mail box)”为空,则会阻塞当前线程直到获得消息为止。基于线程的调度实现起来较为简单,不过基于线程的调度缺点也是非常明显的,由于线程数量受到操作系统的限制,把线程和Actor捆绑起来势必影响到系统中可以同时的Actor数量。而线程数量一多也会影响到系统资源占用以及调度,而在某些情况下大部分的Actor会处于空闲状态,而大量阻塞线程既是系统的负担,也是资源的浪费。因此基于线程的调度是一个拥有重大缺陷的实现,现有的Actor Model大都不会采取这种方式。

于是另一种Actor模型的任务调度方式便是基于事件的调度。“事件”在这里可以简单理解为“消息到达”事件,而此时才会为Actor的任务分配线程并执行。很容易理解,我们现在便可以使用少量的线程来执行大量Actor产生的任务,既保证了运算资源的充分占用,也不会让系统在同时进行的太多任务中“疲惫不堪”,这样系统便可以得到很好的伸缩性。

现有的Actor Model一般都会使用基于事件的调度方式。不过某些实现,如MS CCR、Retlang、Jetlang等类库还需要客户指定资源分配方式,显式地指定Actor与资源池(即线程池)之间的对应关系。而如Erlang或Scala则隐藏了这方面的分配逻辑,由系统整体进行统一管理。前者与后者相比,由于进行了更多的人工干涉,其资源分配可以更加合理,执行效率也会更高——不过其缺点也很明显:会由此带来额外的复杂度。

java actor_Java并发之actor模型相关推荐

  1. Java并发编程实战~Actor 模型

    Hello Actor 模型 Actor 模型本质上是一种计算模型,基本的计算单元称为 Actor,换言之,在 Actor 模型中,所有的计算都是在 Actor 中执行的.在面向对象编程里面,一切都是 ...

  2. java 消息传递示例_java actor模型和消息传递简单示例

    接上面java actor模型框架ujavaactorhttp://zhwj184.iteye.com/admin/blogs/1613351,上面的示例比较复杂,写一个简单点的示例: import ...

  3. akka linux 端口,Actor模型开发库 Akka

    Akka 是一个用 Scala 编写的库,用于简化编写容错的.高可伸缩性的 Java 和 Scala 的 Actor 模型应用. Actor模型并非什么新鲜事物,它由Carl Hewitt于上世纪70 ...

  4. 为什么Actor模型是高并发事务的终极解决方案?

    首先看看道友提出的一个问题: 用户甲的操作 1.开始事务 2.访问表A 3.访问表B 4.提交事务 乙用户在操作 1.开始事务 2.访问表B 3.访问表A 4.提交事务 如果甲用户和乙用户的两个事务同 ...

  5. java actor模型实例,详解Theron通过Actor模型解决C++并发编程的一种思维

    现今,单台机器拥有多个独立的计算单元已经太常见了,这点在服务器的处理器上表现尤为明显,据AMD的一张2012-2013服务器路线图显示,服务器处理器的核心数将在2013年达到20颗之多,合理的利用CP ...

  6. 用JAVA实现基于Actor模型的RPC

    基于Actor模型的RPC ReleaseNote 使用protostuff序列化(.proto文件编写恶心,与Protocol Buffer性能几乎接近) 使用Netty进行通讯(同节点RPC不走网 ...

  7. 深入解析actor 模型(一): actor 介绍及在游戏行业应用

    1 介绍 1.1 什么是actor 对于刚接触actor的我,第一感觉就像redis一样,每个actor就是一个redis 实例,都有自己消息队列,actor相互通信通过将消息发给对方,消息发送进对方 ...

  8. 在.NET中实现Actor模型的不同方式

    上周,<实现领域驱动设计>(Implementing Domain-Driven Design)一书的作者Vaughn Vernon,发布了Dotsero,这是一个使用C#编写的.基于.N ...

  9. java actor_Akka笔记之Actor简介

    英文原文链接,译文链接,原文作者:Arun Manivannan ,译者:有孚 写过多线程的人都不会否认,多线程应用的维护是件多么困难和痛苦的事.我说的是维护,这是因为开始的时候还很简单,一旦你看到性 ...

最新文章

  1. CentOS 6.5 64位 安装Nginx, MySQL, PHP
  2. 非域计算机上模拟域用户,App-V如何让非域内(工作组)PC 也能享受应用程序虚拟化...
  3. Linux 内存管理与系统架构设计
  4. 移动端设备常用尺寸笔记
  5. 生鲜电商/社区团购/团长中心、地址管理、自提点、订单列表、限时折扣、预售、会员储值、钱包、同城配送、门店自提、团长自提、采购、履约、仓储、运输、财务、移动端电商原型、rp源文件、axure电商原型
  6. 3.4 RNN网络扩展:堆叠RNN、递归神经网络、图网络
  7. 神奇的margin之豆瓣豆瓣么么哒
  8. 深入.NET 青鸟影院系统
  9. halcon多模板匹配,每种模板匹配结果不同颜色轮廓
  10. vim 常用配置整理
  11. ea6500 v1 刷梅林_继续测试:Linksys EA6500 v1 的TT固件
  12. 图解LeetCode——854. 相似度为 K 的字符串(难度:困难)
  13. mysql 日历表_如何创建mysql日历表
  14. Mac下嵌入式开发初步(二)
  15. Matlab人工智能算法
  16. 继承 inherit
  17. 201671010434王雯涵--实验二 软件工程个人项目
  18. Adobe Flash CS6 配置错误,错误代码:1
  19. 数学建模——图与网络模型及方法(二)
  20. vcf无法导入iCloud 通讯录

热门文章

  1. CSS超出省略号显示
  2. html flex布局换行,如何用css的flex布局实现换行呢?
  3. 关于亚马逊广告优化如何把握设置细节值得一看?
  4. 掌财社:薇娅再陷售假疑云,直播带货产业链出了什么问题?
  5. 《opencv 数字图像处理 图像基础》
  6. 《计算机操作系统》——1. 操作系统的目标和作用
  7. 机器图像识别常用算法,图像对比识别技术
  8. 未能加载文件或程序集“ypt_ywgd”或它的某一个依赖项。生成此程序集的运行时比当前加载的运行时新,无法加载此程序集。...
  9. STM32基于W5500模块实现通信
  10. 什么是堆、栈以及区别