暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Java并发原理无废话指南(3)

写程序的康德 2016-07-11
160

这是这个系列的第三部分,可能也是最后一部分。第一部分我们从底层分析了并发的两大困难——一致性和有序性;第二部分我们从底层分析了JVM中并行开发的两大技术——线程和锁。正如文章的标题所暗示的——我希望分析原理而不是告诉你如何操作。(如何使用Java并发包只要Google一下一抓一大把)这是这个系列的第三部分,可能也是最后一部分。

第三部分我们重新思考——除了线程和锁是不是有更高级的并发工具?在Java中基于线程的我们称为Java Thread模型,除此之外还有一种模型叫Actor模型。

面向对象的并发

面向对象是一种对现实世界的抽象模型,它把我们感兴趣的属性封装起来,定义一组操作供外部调用(消息传递)。Actor和它是相同的设计思路。根据我的考证,面向对象鼻祖式语言——Smalltalk就是参考了Actor实现的“消息传递”( message-passing)。所以说“Actor是更面向对象的并发模型”不如说是面向对象的回归。

在Actor模型中并发实体不再是线程或者进程而是由应用框架定义的并发实体(相比较而言线程和进程是由内核定义的)。在Java并没有内置Actor模型,最为著名的实现是Akka。我们看一个例子


每个Actor都有一个“邮箱”,当需要它完成某个事情的时候只需要向它的邮箱发送消息。Akka会在适当的时候调度这个Actor,处理邮箱的数据。这个模式有很多名称——消息驱动,命令驱动,事件驱动。

代码中我定义了PingActor,PongActor,PingActor收到pong的消息时会回复ping;PongActor收到ping回复pong。程序会在PingActor收到100条pong后结束运行。

代码中我们虽然定义了Actor但是始终都持有Actor的引用——ActorRef,而不是实际的对象;每个Actor都必须实现onReceive方法来响应请求;当收到消息后sender就是消息发送者的ActorRef,self则表示自己的ActorRef。如果把Actor和操作系统的线程做类比,可以吧ActorRef理解为线程的ID。

Akka的原理

用一副官方的图来表示:

Akka会定义一个线程池(有很多实现,ThreadPoolExecutor、FockJoinPool),每个线程池都会维护一组MailQueue(有很多实现ConcurrentLinkedQueue、PriorityBlockingQueue),MailQueue的数量和Actor是一一对应的,当你调用tell的时候就相当于往MailQueue放一条数据。Dispatcher侦测到这个变化然后使用线程池里空闲的线程执行Actor的onReceive方法。

所有Akka的实现还是依赖于Java底层的并发包。(Thread模型)

Actor特点

  • 面向对象,不用考虑线程,锁之类的问题。只要定义好并发实体彼此之间的消息,就可以了。有人也称这个为“面向并发”

  • 编程简单,只需要实现onReceive,响应消息,把处理结果发送出去就可以了。所以编程模型非常简单。

  • 轻量级并发实体,Acotr不是线程,所以不受操作系统线程数量的限制。它是一个Java对象,只占用很少的内存空间,新建它是new一个对象,销毁它是把内存空间释放掉。所以比起线程的创建、回收所占用的资源要小很多。

等等,还没结束

夸赞完Actor模型之后按照常理你可能天真的以为我会推荐Akka和Scala或者把其他函数式编程语言也给吹捧一下。可惜你错了,我不会推荐Akka、Scala,也不会推荐任何函数式编程语言,我还会捎带把Go也给嘲讽一下。

Actor是独立于操作系统实现的一种并发实体,实现这个的难度非常高,如果仅仅是像某些“函数式语言”一样只是模仿皮毛而没有注意到本质那只能成为一个——玩具而已。

CPU是由操作系统管理的,操作系统会把CPU分配给线程使用。操作系统可以自己决定哪个线程暂停让出CPU,哪个线程得到CPU,这个叫调度。一般情况下现代操作系统都是抢占式调度

如果我们想在“应用层”实现并发实体,那么也要面临这个问题——如何调度。更要命的是——如何支持抢占式调度?答案是:除了小心翼翼的主动让出CPU之外没有别的办法。

举个例子:Java中执行代码的时候会主动让出CPU吗?(Java中让出CPU需要调用Thread.yield)——当然不会,除非操作系统剥夺CPU,一帮情况很少有主动让出CPU的。比如:当我们在一个Actor执行比较大的M5计算时CPU会被Actor吃掉,Akka根本没有办法让Actor让出CPU(能做到这一点的只有上帝——操作系统)这就导致了——无可抢占。(就像炫迈,根本停不下来)

所以如果想实现一个真正的Actor,必须从底层考虑——可抢占。你必须仔细在所有的函数中插入“主动让出CPU”的Thread.yield语句。毫无疑问——Java根本没有考虑,Scala也没有考虑,Akka也没有考虑,所有JVM上的Actor模型都没有考虑,也没有办法做这样的事情。当然——Go语言考虑了,但是明显不全,而且实现起来更难。(什么?Nodejs?这个也敢算Actor?)

全世界真正支持Actor模型的只有一种语言——Erlang,除此之外没有第二个。所以你现在明白了为什么“全世界程序员都错了,只有Erlang程序员是对”的这句话的真正含义了吗?对,大家都跑偏了,沉迷于漂亮的封装,牛B哄哄而又毫无价值的“脑力游戏”中;只有Erlang程序员静静的看着我们装B。

我的推荐

Actor模型是一个很好的模型,除非你有清楚自己在做什么否则不要轻易在JVM上尝试。Java的Thread模型已经非常牛B,非常简洁了。好好学习并发包,做一个乖孩子。如果你真的需要一个Actor模型,毫无疑问你应该使用Erlang,因为除了它之外其他的都是“学术上的Actor”。

文章转载自写程序的康德,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论