对于软件开发人员而言,调试多线程应用程序中的非确定缺陷是最痛苦的工作。因此,像大多数人一样,我钟爱使用 Erlang 和 Scala 等函数语言进行并发编程。
Scala 和 Erlang 都采用了角色模型来进行并发编程,没有采用线程概念。围绕角色模型的创新并不仅限于语言本身,角色模型也可供 Kilim 等基于 Java 的角色框架使用。
Kilim 对角色模型的使用非常直观,稍后您将看到,该库使构建并发应用程序变得异常简单。
在 2005 年,Herb Sutter 编写了一篇现在仍然颇为著名的文章 “The Free Lunch is Over: A Fundamental Turn Toward Concurrency in Software”。在这篇文章中,他摒弃了一直误导着人们的观念,那就是摩尔定律将继续促进越来越高的 CPU 时钟速率。
Sutter 预言了 “免费午餐” 的终结,通过越来越快的芯片来捎带提升软件应用程序的性能将不再可能。相反,他认为应用程序性能的显著提升将需要利用多核芯片架构来实现。
事实证明他是对的。芯片制造商已经达到了一种硬性限制,芯片速率已稳定在 3.5 GHz 左右多年了。随着制造商越来越快地增加芯片上的核心数量,摩尔定律在多核领域继续得以满足。
关于本系列
自 Java 技术首次诞生以来,Java 开发格局已发生了翻天覆地的变化。得益于成熟的开源框架和可靠的租赁部署基础设施,现在可以迅速而经济地组装、测试、运行和维护 Java 应用程序。在本 系列 中,Andrew Glover 探索使这种新的 Java 开发风格成为可能的各种技术和工具。
Sutter 还提到并发编程将使开发人员能够利用多核架构。但是,他补充道,“相比如今的各种语言提供的编程模型,我们亟需一种更高级的并发编程模型。”
Java 等语言的基本编程模型是基于线程的。尽管多线程应用程序并不是很难编写,但正确 编写它们仍然面临许多挑战。并发编程的一个困难之处是利用线程来考虑并发性。如今已有许多并发模型,一种特别有趣并获得了 Java 社区认可的模型就是角色模型。
角色模型是一种不同的并发进程建模方式。与通过共享内存与锁交互的线程不同,角色模型利用了 “角色” 概念,使用邮箱来传递异步消息。在这里,邮箱 类似于实际生活中的邮箱,消息可以存储并供其他角色检索,以便处理。邮箱有效地将各个进程彼此分开,而不用共享内存中的变量。
角色充当着独立且完全不同的实体,不会共享内存来进行通信。实际上,角色仅能通过邮箱通信。角色模型中没有锁和同步块,所以不会出现由它们引发的问题,比如死锁、严重的丢失更新问题。而且,角色能够并发工作,而不是采用某种顺序方式。因此,角色更加安全(不需要锁和同步),角色模型本身能够处理协调问题。在本质上,角色模型使并发编程更加简单了。
角色模型并不是一个新概念,它已经存在很长时间了。一些语言(比如 Erlang 和 Scala)的并发模型就是基于角色的,而不是基于线程。实际上,Erlang 在企业环境中的成功(Erlang 由 Ericsson 创建,在电信领域有着悠久的历史)无疑使角色模型变得更加流行,曝光率更高,而且这也使它成为了其他语言的一种可行的选择。Erlang 是角色模型更安全的并发编程方法的一个杰出示例。
不幸的是,角色模型并没有植入到 Java 平台中,但我们可以通过各种方式使用它。JVM 对替代语言的开放性意味着您可以通过 Java 平台语言(比如 Scala 或 Groovy)来利用角色(参见 参考资料,了解 Groovy 的角色库 GPars)。另外,您可以试用一种支持角色模型且基于 Java 的库,比如 Kilim。
Kilim 是一个使用 Java 编写的库,融入了角色模型的概念。在 Kilim 中,“角色” 是使用 Kilim 的 Task
类型来表示的。Task
是轻量型的线程,它们通过 Kilim 的 Mailbox
类型与其他 Task
通信。
Mailbox
可以接受任何类型的 “消息”。例如,Mailbox
类型接受 java.lang.Object
。Task
可以发送 String
消息或者甚至自定义的消息类型,这完全取决于您自己。
在 Kilim 中,所有实体都通过方法签名捆绑在一起,如果您需要同时执行几项操作,可以在一个方法中指定该行为,扩大该方法的签名以抛出 Pausable
。因此,在 Kilim 中创建并发类就像在 Java 中实现 Runnable
或扩展 Thread
一样简单。只是使用 Runnable
或Thread
的附加实体(比如关键字 synchronized
)更少了。
最后,Kilim 的魔力是由一个称为 weaver 的后期进程来实现的,该进程转换类的字节码。包含 Pausable
throws
字句的方法在运行时由一个调度程序处理,该调度程序包含在 Kilim 库中。该调度程序处理有限数量的内核线程。可以利用此工具来处理更多的轻量型线程,这可以最大限度地提高上下文切换和启动的速度。每个线程的堆栈都是自动管理的。
在本质上,Kilim 使创建并发进程变得轻松而简单:只需从 Kilim 的 Task
类型进行扩展并实现 execute
方法。编译新创建的支持并发性的类之后,对其运行 Kilim 的 weaver,您会实现显著的性能提升!
Kilim 最初是一种外来语言,但它带来了巨大的回报。角色模型(以及后来的 Kilim)使编写依赖于类似对象的异步操作对象变得更加简单和安全。您可以 使用 Java 的基本线程模型进行同样的操作(比如扩展 Thread
),但这更具挑战性,因为它会将您带回锁和同步的世界中。简而言之,将您的并发编程模型转换为角色使多线程应用程序更容易编码。
在 Kilim 的角色模型中,消息通过 Mailbox
在进程之间传送。在许多情况下,您可以将 Mailbox
看作队列。进程可以将一些项加入邮箱中,也可以从邮箱获取一些项,而且它们既可以采用阻塞方式,也可以采用非阻塞方式来这样做(阻塞对象是底层 Kilim 实现的轻量型进程,而不是内核线程)。
作为在 Kilim 中利用邮箱的一个示例,我编写了两个角色(Calculator
和 DeferredDivision
),它们从 Kilim 的 Task
类型扩展而来。这些类将以一种并发方式协同工作。DeferredDivision
对象将创建一个被除数和一个除数,但它不会尝试将这两个数相除。我们知道除法运算很耗资源,所以 DeferredDivision
对象将要求 Calculator
类型来处理该任务。
这两个角色通过一个共享 Mailbox
实例通信,该实例接受一个 Calculation
类型。这种消息类型非常简单 —— 已提供了被除数和除数,Calculator
随后将执行计算并设定相应的答案。Calculator
然后将这个 Calculation
实例放回共享 Mailbox
中。
清单 1 给出了这个简单的 Calculation
类型。您会注意到这个类型不需要任何特殊的 Kilim 代码。实际上,它只是一个再普通不过的 Java bean。
清单 1. 一个 Calculation 类型的消息
import java.math.BigDecimal; public class Calculation { private BigDecimal dividend; private BigDecimal divisor; private BigDecimal answer; public Calculation(BigDecimal dividend, BigDecimal divisor) { super(); this.dividend = dividend; this.divisor = divisor; } public BigDecimal getDividend() { return dividend; } public BigDecimal getDivisor() { return divisor; } public void setAnswer(BigDecimal ans){ this.answer = ans; } public BigDecimal getAnswer(){ return answer; } public String printAnswer() { return "The answer of " + dividend + " divided by " + divisor + " is " + answer; } } |
DeferredDivision
类中使用了特定于 Kilim 的类。该类执行多项操作,但总体来讲它的工作非常简单:使用随机数(类型为BigDecimal
)创建 Calculation
的实例,将它们发送到 Calculator
角色。而且,该类还会检查共享的 MailBox
,以查看其中是否有任何 Calculation
。如果检索到的一个 Calculation
实例有一个答案,DeferredDivision
将打印它。
清单 2. DeferredDivision 创建随机除数和被除数
import java.math.BigDecimal; import java.math.MathContext; import java.util.Date; import java.util.Random; import kilim.Mailbox; import kilim.Pausable; import kilim.Task; public class DeferredDivision extends Task { private Mailbox<Calculation> mailbox; public DeferredDivision(Mailbox<Calculation> mailbox) { super(); this.mailbox = mailbox; } @Override public void execute() throws Pausable, Exception { Random numberGenerator = new Random(new Date().getTime()); MathContext context = new MathContext(8); while (true) { System.out.println("I need to know the answer of something"); mailbox.putnb(new Calculation( new BigDecimal(numberGenerator.nextDouble(), context), new BigDecimal(numberGenerator.nextDouble(), context))); Task.sleep(1000); Calculation answer = mailbox.getnb(); // no block if (answer != null && answer.getAnswer() != null) { System.out.println("Answer is: " + answer.printAnswer()); } } } } |
从清单 2 可以看到,DeferredDivision
类扩展了 Kilim 的 Task
类型,后者实际上模仿了角色模型。注意,该类还改写了 Task
的execute
方法,后者默认情况下抛出 Pausable
。因此,execute
的操作将在 Kilim 的调度程序控制下进行。也就是说,Kilim 将确保execute
以一种安全的方式并行地运行。
在 execute
方法内部,DeferredDivision
创建 Calculation
的实例并将它们放在 Mailbox
中。它使用 putnb
方法以一种非阻塞方式完成此任务。
填充 mailbox
后,DeferredDivision
进入休眠状态 —— 注意,与处于休眠状态的内核线程不同,它是由 Kilim 托管的轻量型线程。当角色唤醒之后,像前面提到的一样,它在 mailbox
中查找任何 Calculation
。此调用也是非阻塞的,这意味着 getnb
可以返回null
。如果 DeferredDivision
找到一个 Calculation
实例,并且该实例的 getAnswer
方法有一个值(也就是说,不是一个已由Calculator
类型处理过的 Calculation
实例),它将该值打印到控制台。
Mailbox
的另一端是 Calculator
。与清单 2 中定义的 DeferredDivision
角色类似,Calculator
也扩展了 Kilim 的 Task
并实现了execute
方法。一定要注意两个角色都共享同一个 Mailbox
实例。它们不能与不同的 Mailbox
通信,它们需要共享一个实例。相应地,两个角色都通过它们的构造函数接受一个有类型 Mailbox
。
清单 3. 最终的实际运算角色:Calculator
import java.math.RoundingMode; import kilim.Mailbox; import kilim.Pausable; import kilim.Task; public class Calculator extends Task{ private Mailbox<Calculation> mailbox; public Calculator(Mailbox<Calculation> mailbox) { super(); this.mailbox = mailbox; } @Override public void execute() throws Pausable, Exception { while (true) { Calculation calc = mailbox.get(); // blocks if (calc.getAnswer() == null) { calc.setAnswer(calc.getDividend().divide(calc.getDivisor(), 8, RoundingMode.HALF_UP)); System.out.println("Calculator determined answer"); mailbox.putnb(calc); } Task.sleep(1000); } } } |
Calculator
的 execute
方法与 DeferredDivision
的相应方法一样,不断循环查找共享 Mailbox
中的项。区别在于 Calculator
调用get
方法,这是一种阻塞调用。相应地,当一条 Calculation
“消息” 显示时,它执行请求的除法运算。最后,Calculator
将修改的Calculation
放回到 Mailbox
中(采用非阻塞方式),然后进入休眠状态。两个角色中的休眠调用都仅用于简化控制台的读取。
在前面,我提到了 Kilim 通过其 weaver 执行字节码操作。这是一个简单的后处理过程,您在编译了类之后 运行它。weaver 然后将一些特殊代码添加到包含 Pausable
标记的各种类和方法中。
调用 weaver 非常简单。举例而言,在清单 4 中,我使用 Ant 调用 Weaver。我需要做的只是告诉 Weaver 我需要的类在哪里,以及在哪里放置生成的字节码。在这个例子中,我让 Weaver 更改 target/classes
字典中的类,并将生成的字节码写回到该字典。
清单 4. Ant 调用 Kilim 的 weaver
<target name="weave" depends="compile" description="handles Kilim byte code weaving"> <java classname="kilim.tools.Weaver" fork="yes"> <classpath refid="classpath" /> <arg value="-d" /> <arg value="./target/classes" /> <arg line="./target/classes" /> </java> </target> |
更改代码之后,我就可以在运行时随意利用 Kilim 了,只要我在类路径中包含了它的 .jar 文件。
将这两个角色应用到实际中就像在 Java 代码中应用两个普通的 Thread
一样。您使用同一个共享 sharedMailbox
实例创建并扩展两个角色实例,然后调用 start
方法来实际设置它们。
清单 5. 一个简单的程序
import kilim.Mailbox; import kilim.Task; public class CalculationCooperation { public static void main(String[] args) { Mailbox<Calculation> sharedMailbox = new Mailbox<Calculation>(); Task deferred = new DeferredDivision(sharedMailbox); Task calculator = new Calculator(sharedMailbox); deffered.start(); calculator.start(); } } |
运行这两个角色会得到如清单 6 所示的输出。如果运行此代码,您的输出可能有所不同,但活动的逻辑顺序将保持不变。在清单 6 中,DeferredDivision
请求计算,Calculator
使用一个答案作为响应。
清单 6. 您的输出将有所不同 —— 各个角色不是一成不变的
[java] I need to know the answer of something [java] Calculator determined answer [java] Answer is: The answer of 0.36477377 divided by 0.96829189 is 0.37671881 [java] I need to know the answer of something [java] Calculator determined answer [java] Answer is: The answer of 0.40326269 divided by 0.38055487 is 1.05967029 [java] I need to know the answer of something [java] Calculator determined answer [java] Answer is: The answer of 0.16258913 divided by 0.91854403 is 0.17700744 [java] I need to know the answer of something [java] Calculator determined answer [java] Answer is: The answer of 0.77380722 divided by 0.49075363 is 1.57677330 |
角色模型支持采用一种更安全的机制来在进程(或角色)之间进行消息传递,极大地方便了并发编程。此模型的实现因语言和框架的不同而不同。我建议参考 Erlang 的角色,其次是 Scala 的角色。两种实现都很简洁,都具有各自的语法。
如果您想要利用 “plain Jane” Java 角色,那么您最好的选择可能是 Kilim 或一种类似框架(参见 参考资料)。世上没有免费的午餐,但基于角色的框架确实使并发编程以及利用多核进程变得更加简单。
学习
- “A Fundamental Turn Toward Concurrency in Software”(Herb Sutter,Dr. Dobb's Journal,2005 年 3 月):Herb Sutter 最初介绍新的并发编程方法的文章。
- “More Java Actor Frameworks Compared”(Salmon Run,2009 年 1 月):Blogger Sujit Pal 比较基于 Java 与基于 Scala 的角色框架 Kilim、Jetlang、ActorFoundry 和 Actors Guild。
- “Understanding actor concurrency, Part 1: Actors in Erlang”(Alex Miller,JavaWorld,2009 年 2 月):进一步探索 Erlang 和 Scala 实现的角色模型。
- “Crossing borders: Concurrent programming with Erlang”(Bruce Tate,developerWorks,2006 年 4 月):找到使 Erlang 在并发编程、分布式系统和软实时系统领域流行的背后因素。
- “面向 Java 开发人员的 Scala 指南:深入了解 Scala 并发性”(Ted Neward,developerWorks,2009 年 2 月):Ted Neward 深入剖析 Scala 语言和环境提供的各种并发性特征和库。
- 进一步了解 Kilim:Kilim 创建者 Sriram Srinivasan 维护此页面,其中包括 Kilim 的简介,以及白皮书、教程和视频演示的链接。
- 浏览 技术书店,获取关于这些或其他技术主题的图书。
- developerWorks Java 技术专区:找到数百篇关于 Java 编程各个方面的文章。
获得产品和技术
- GPars - Groovy 并行系统:开始使用 Groovy 基于角色的框架在 Java 平台上进行并发编程。
- 下载 Kilim:一种 Java 消息传递框架,提供了超轻量级的线程,推动了线程之间迅速、安全、无需复制的消息传递的实现。
讨论
- Amazon Web 服务讨论论坛:加入 EC2 讨论论坛。
- 加入 My developerWorks 社区。
Andrew Glover 是一名开发人员、作家、演讲家和企业家,他对行为驱动开发、持续集成和敏捷软件开发有巨大的热情。可以通过他的 博客 关注他。