Java 开发 2.0: Kilim 简介
对于软件开发人员而言,调试多线程应用程序中的非确定缺陷是最痛苦的工作。因此,像大多数人一样,我钟爱使用 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 左右多年了。随着制造商越来越快地增加芯片上的核心数量,摩尔定律在多核领域继续得以满足。
角色模型是一种不同的并发进程建模方式。与通过共享内存与锁交互的线程不同,角色模型利用了 “角色” 概念,使用邮箱来传递异步消息。在这里,邮箱 类似于实际生活中的邮箱,消息可以存储并供其他角色检索,以便处理。邮箱有效地将各个进程彼此分开,而不用共享内存中的变量。
角色充当着独立且完全不同的实体,不会共享内存来进行通信。实际上,角色仅能通过邮箱通信。角色模型中没有锁和同步块,所以不会出现由它们引 发的问题,比如死锁、严重的丢失更新问题。而且,角色能够并发工作,而不是采用某种顺序方式。因此,角色更加安全(不需要锁和同步),角色模型本身能够处 理协调问题。在本质上,角色模型使并发编程更加简单了。
角色模型并不是一个新概念,它已经存在很长时间了。一些语言(比如 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。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 创建随机除数和被除数
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<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
方法来实际设置它们。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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. 您的输出将有所不同 —— 各个角色不是一成不变的
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
[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 编程各个方面的文章。