本文系外部转帖。主要概述kilim是一种角色模型以及具体的实战demo。
中文地址:https://www.ibm.com/developerworks/cn/java/j-javadev2-7.html
英文原文地址:https://www.ibm.com/developerworks/library/j-javadev2-7/j-javadev2-7-pdf.pdf
demo 实现地址:https://github.com/zy416548283/JavaBaseStudy/tree/master/src/main/java/com/tencent/java/kilim/intro
对于软件开发人员而言,调试多线程应用程序中的非确定缺陷是最痛苦的工作。因此,像大多数人一样,我钟爱使用 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 左右多年了。随着制造商越来越快地增加芯片上的核心数量,摩尔定律在多核领域继续得以满足。
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 的后期进程来实现的,该进程转换类的字节码。包含 Pausablethrows 字句的方法在运行时由一个调度程序处理,该调度程序包含在 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 中。
如下给出了这个简单的 Calculation 类型。您会注意到这个类型不需要任何特殊的 Kilim 代码。实际上,它只是一个再普通不过的 Java bean。
package com.tencent.java.kilim.intro;
import java.math.BigDecimal;
/**
* java bean: 被除数、除数、result
* @author andy
*
*/
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 将打印它。
package com.tencent.java.kilim.intro;
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 mailbox;
public DeferredDivision(Mailbox 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());
}
}
}
}
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。与上面定义的 DeferredDivision 角色类似,Calculator 也扩展了 Kilim 的 Task 并实现了 execute 方法。一定要注意两个角色都共享同一个 Mailbox 实例。它们不能与不同的 Mailbox 通信,它们需要共享一个实例。相应地,两个角色都通过它们的构造函数接受一个有类型 Mailbox。
package com.tencent.java.kilim.intro;
import java.math.RoundingMode;
import kilim.*;
public class Calculator extends Task {
private Mailbox mailbox;
public Calculator(Mailbox 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 字典中的类,并将生成的字节码写回到该字典。
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 方法来实际设置它们。
package com.tencent.java.kilim.intro;
import kilim.*;
/**
* 参考文章:kilim简介:https://www.ibm.com/developerworks/cn/java/j-javadev2-7.html
* @author andy
*
*/
public class CalculationCooperation {
public static void main(String[] args) {
Mailbox sharedMailbox = new Mailbox();
Task deferred = new DeferredDivision(sharedMailbox);
Task calculator = new Calculator(sharedMailbox);
deferred.start();
calculator.start();
}
}
运行这两个角色会得到如清单 6 所示的输出。如果运行此代码,您的输出可能有所不同,但活动的逻辑顺序将保持不变。在清单 6 中,DeferredDivision 请求计算,Calculator 使用一个答案作为响应。
[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 或一种类似框架(参见 参考资料)。世上没有免费的午餐,但基于角色的框架确实使并发编程以及利用多核进程变得更加简单。