1
多线程并发的难题
张大胖在做一个银行相关的项目,写了一个 Account 的类,用来表示一个用户的银行账号,根据银行的常规业务,自然要提供两个方法,存款(deposit)和取款(withdraw)。
为了防止多线程并发时导致的数据不一致问题,张大胖给每个方法都加了 synchronized, 那意思很清楚,想进入某个方法执行存款或取款操作,必须得先获得一把锁才行。
(注:为了简化,这里没有做边界条件检查。)
但是在做转账操作的时候,为了保证一致性,必须得把两个账户都加上锁,然后才可以操作,于是张大胖写下了这样的代码,他觉得很简单,立刻就提交给 Bill ,让他 Review。
富有经验的 Bill 立刻就发现了问题,马上对张大胖说:“这样会出现死锁!”
张大胖说:“这么简单的代码,怎么可能有死锁?”
“假设线程 1 做的操作是账户 A 给账户 B 转账, 先锁住了 A 账户, 接下来试图申请 B 账户的锁;
与此同时线程 2 在从 账户 B 给账户 A 转账, 先锁住了 B 账户的锁, 接下来试图申请 A 账户的锁。
两个线程各自持有资源, 然后等待获取对方的资源, 都无法执行下去, 死锁就出现了!”
张大胖无言以对,不得不承认 Bill 是正确的。他问道:“那怎么解决这个问题?”
“非常简单,加锁的时候按次序来就可以了,例如所有的线程,无论是从 A 向 B 转账,还是从 B 向 A 转账,都先获得账号 A 的锁,成功后再获得账户 B 的锁,这样就没问题了。”
张大胖说:“那样代码会变得很古怪啊,还得给两个账户排个顺序,如果不知道背后的思想读起来很痛苦,怪不得人家说多线程编程很难啊。”
Bill 说:“是啊, 其实线程这个东西,就是一段代码的执行而已, 是操作系统层面的概念,可是我们苦逼的程序员不得不来面对它,来背这个多线程并发的锅了。”
2
黑盒子
下班后,张大胖一直在思考这个问题:既然线程是操作系统层面的概念,能不能把线程的概念隐藏起来,然后所有的操作都不用加锁呢? 这样以来编程就会容易得多啊!
本质的问题是什么?
首先是共享的状态,例如 Account 中的 balance ,多个线程都要读写, 其次就是多个线程乱序、并发执行。
能不能换个思路,把这个 Account 对象看成一个黑盒子,你想存款了,就发一个存款的消息过来,想取款就发一个取款的消息过来。
不管是有一个消息,还是有 100 个消息,我统统放到黑盒子的一个队例中,然后让 Account 对象一个个顺序处理不就可以了? 根本不用在方法上加锁!
这样做,其实就是把并发的操作变成了串行的操作而已!
不对,如果调用方把取款消息放下就走, 不等待返回结果, 那就不是同步操作,而是异步操作了!
但是如果取款的时候发现余额不足,怎么通知调用方?嗯,调用方也必须是个黑盒子对象,也向它发送异步消息,这个消息也会在消息队列中存下来,调用方“黑盒子”也会一个个处理。
想到这一层,张大胖激动起来:取款和存款的操作就不用在加锁了,码农们只要考虑黑盒子对消息的处理即可:取出消息,处理消息,向别的黑盒子发送消息, 根本不用考虑线程这样底层的概念了。
3
Actor 模型
第二天张大胖赶紧找到 Bill, 向他炫耀自己的“新发明”。
Bill 不动声色:“小伙子,不错啊,重新发明了轮子!”
“重新发明?”
“是啊,你这个所谓黑盒子,就是所谓 Actor 模型啊! 它最早由 Carl Hewitt 在 1973 定义,其消息传递的方式更加符合面向对象的原始意图, 这一点我想你也体会到了,要不你怎么把他们叫做黑盒子啊。”
“1973 年? 我还没出生。唉,看来这些概念已经被老前辈们都发明完了啊。”
“Actor 属于并发组件模型 ,可以把程序员从多线程并发或线程池等基础概念中解放出来。它有这么几个特点:”
Actor:
就是你说的黑盒子,系统是由很多 Actor 组成。 Actor 之间不共享状态,但是会接收别的 Actor 发送的异步消息,处理的过程中,会改变内部状态,也可能向别的 Actor 发送消息。
Message:
消息是不可变的, 它的发送都是异步的,Actor 内部有个“MailBox”来缓存消息。
MailBox:
Actor 内部缓存消息的邮箱, 其他 Actor 发送的消息都放到这里,然后被本 Actor 处理,类似有多个生成者和一个消费者的队例。
张大胖说:“和我之前的图差不多,看来我确实是重新发明了轮子啊。”
4
用 Actor 实现转账
Bill 笑道:“这个 Actor 看起来很美,但是编程的时候你得刷新一下你的思维才行。 大胖,之前你的转账操作在多线程下不是会出现死锁吗? 你考虑下,如果用 Actor 的思路该怎么写?”
“首先,得有两个 Actor, 这两个 Actor 表示了两个账户,我把它们叫做旺财和小强。”
“然后呢,转账的逻辑怎么处理?”
张大胖想了一会:“既然转账是在两个 Actor 之间发生的,那可以引入一个协调者 Actor,叫做转账管家吧。不过,由于消息都是异步的,转账管家向旺财这个 Actor 发起扣款请求以后,不知道什么时候才能真正执行扣款,也不能立刻知道是否成功,必须得等待啊,这就有点麻烦了。”
Bill 说:“我给你画个流程图,你看看。”
张大胖感慨地说:“原来的多线程并发模型,需要同时锁住两个账户,然后才能进行转账。现在每个 Actor 都独立,也把这个转账给搞定了。”
Bill 说:“其实对于转账管家来说,对每个转账的消息,内部是隐含一个流程状态的,就是先向某个账户扣款,成功以后再向另一个账户增加,最后给调用者返回状态,这个次序是不能乱的。看到图中那个 Transaction ID 没有(Tx01),就是用来跟踪这个转账的事务。”
4
漏洞
“我发现了一个漏洞,你这个转账虽然看起来很美,没有加锁,但是和原来的是有区别的,原来多线程思路是会把旺财和小强的账户同时锁住,然后转账,在这个过程中,别人是不能操作这两个账号的! 而你的 Actor 方案中,当转账管家给旺财发消息扣款的时候,小强其实是自由的,如果这时候小强的账户被冻结,那你的转账管家还得回滚旺财的扣款,这多麻烦啊。”
Bill:“哈哈,你小子还挺机灵的嘛,看出了这个问题,Actor 模型非常适用于多个组件独立工作,相互之间仅仅依靠消息传递的情况。如果想在多个组件之间维持一致的状态(比如咱们例子中的转账),那就不爽了。”
“那怎么解决这个问题?”
“那必须得用一些特殊手段了,有些实现 Actor 的框架,例如 Akka,专门提供了像 Coordinated /Transactor 这样的机制来处理这个问题。有空的话给你仔细讲讲。”
“好吧,我回头看看这个 Akka, 对了, Actor 虽然对用户隐藏了线程, 但是总得有线程来处理消息吧。” 张大胖问道。
“那是肯定的,线程本质上就是一段代码的执行,每个 Actor 在处理消息的时候,肯定得和线程关联才行,只不过 Actor 系统把线程这个概念给隐藏了。”
“有哪些系统实现了 Actor?” 张大胖接着问。
“其实最著名的就是 Erlang 了,Actor 模型可以说是它的基础,除了我们上面所说的,还可以让 Actor 之间建立关联,例如让一个 Actor 去监控另外一些 Actor 工作,如果那些 Actor 崩溃了,就新建一个 Actor 继续工作。在 Java 领域,刚才提到的 Akka 是比较知名的一个 Actor 框架。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于