【消息队列】——如何使用Actor模型解决并发问题
目录
- 一、前提
- 二、共享内存模型面临哪些挑战?
- 2.1、比如并发读写的线程安全问题
- 2.2、比如竞争条件下的数据一致性问题。
- 2.3、同步机制可以解决线程安全和竞争条件问题,但也带来了一系列新问题。
- 三、Actor 模型如何解决多线程并发问题?
- 四、Actor 模型的优劣势和适用场景
- 4.1、Actor 不适用于高并发场景
- 4.2、使用 Actor 模型的实践经验
- 五、小结
本文来源:极客时间vip课程笔记
一、前提
- 对 RabbitMQ 有所了解的同学都知道,RabbitMQ 使用的开发语言是一种相对小众且“古老”的 Erlang 语言。Erlang 语言采用了一种和其他编程语言都很不一样的 Actor 模型。此外,在大数据领域被广泛使用的 Apache Flink 和 Apache Spark 也都采用了基于 Actor 模型构建的 Akka 分布式框架来实现。
- 我在实现一些较复杂的多线程并发场景时,尝试使用 Actor 模型,也感受到了这种编程模型的魅力。它可以更简单地解决并发编程所遇到的一系列共享资源访问冲突、时序控制和一致性的难题。
- 更重要的是,Actor 模型更加安全,不需要开发者有丰富的并发编程经验,就很容易写出健壮的代码。
二、共享内存模型面临哪些挑战?
- 为了理解 Actor 模型,我们先来看看在不使用 Actor 模型时,如何处理多线程并发问题。
- 当我们使用面向对象语言开发时,一个类可以包含一些属性,通俗地说就是对象的字段,比如一个用户类,它的属性可以有用户名、手机号等等。当类实例化成一个一个对象时,每个对象的属性就保存了这个对象的状态。正是这些状态,才使得每个对象都独一无二。
- 按照面向对象的封装原则,对象的状态理论上不应对外暴露,但在实际开发中,很多类的属性都需要通过 getter 和 setter 方法暴露出来供外部访问。
- 在多线程并发场景下,有些对象需要供多个线程来并发访问,比如,一切全局的单例对象、全局的配置数据、内存中的缓存数据等等,都面临多线程并发访问的问题。
- 因为在运行时,每个对象和它的状态都占用特定的一块儿内存区域,所以这种我们习以为常的编程模型也被称为“共享内存模型”。
- 有过多线程开发经验的同学都知道,并发访问共享对象,在开发调试过程中会面临很多的挑战。
2.1、比如并发读写的线程安全问题
- 对大部分集合和复杂对象来说,读写都不是原子性的,多个线程并发访问的时候,会破坏数据的完整性。
- 像多个线程同时更新一个对象时,你写这一部分,我写那一部分,结果是把整个数据结构写坏了;或者,一个线程在这一点儿一点儿地读数据,另外一个线程在更新这个数据,那个读的线程读到数据可能是一部分新的,一部分旧的,读到的结果是新旧内容参半的错误数据。
2.2、比如竞争条件下的数据一致性问题。
- 在更新一个共享对象之前,通常都需要先读取对象,计算后再更新对象的某些属性。如果同时有另外一个线程也在更新这个对象,就会发生互相覆盖更新。先更新的数据被后更新的数据给覆盖掉,导致先更新的那部分内容失效。
- 如果这种覆盖更新并非我们所预期,那恭喜你喜提一枚 Bug。
- 理论上,这两个问题都可以通过锁、信号量或屏障等类似的同步机制来解决。这些同步机制本质上是将对共享内存的并行访问变成串行访问,在共享内存之前设置一道门,每次只能进入一个线程,当共享内存被占用时,其他需要访问的线程只能在门口等待。