首页 » Java程序员修炼之道 » Java程序员修炼之道全文在线阅读

《Java程序员修炼之道》9.6 actor介绍

关灯直达底部

Java的显式锁和同步模型刻下了岁月的痕迹。在最初设计Java语言时,它是一个奇妙的创新,但也埋下了祸根。Java并发模型本质上是面对两难境地时采取折中策略的产物。

锁太少,会导致并发代码不安全,出现竞态条件。锁太多,系统会丧失活力,代码瘫痪,工作毫无进展。这就是我们在第4章讨论过的,安全性与系统活力之间的矛盾。

使用基于锁的模型,必须照顾到给定时间内所有可能发生的并发操作。但随着程序变得越来越大,要做到滴水不漏会变得越来越困难。尽管Java有办法缓解一些问题,但核心问题还在,如果Java语言不能发布一个拒绝向后兼容的版本,就不可能从根本上解决这个问题。

非Java语言有机会从头开始。备选语言可以不向程序员暴露锁和线程的底层细节,而是在自己的运行时环境中提供额外的并发支持。

这应该没什么好奇怪的。毕竟在Java刚刚出现时,Java内存模型就受到过质疑。当时很多C和C++开发人员都对这种想法感到诧异,怎么能由运行时负责管理内存,而让开发人员远离这些细节呢?

我们来看一下Scala基于actor技术的并发模型,看它如何让并发编程变了样(也更简单)。

9.6.1 代码大舞台

actor是扩展scala.actors.Actor,并实现了act方法的对象。希望这个定义能跟你脑海中对Java线程的定义相呼应。它们最大的差别就是actor在大多数情况下都不会通过共享的数据进行沟通。

程序员在共享数据时必须采用最佳实践。如果你想在actor间共享状态,Scala不会阻止你。我们只是认为这么做不好。actor有沟通的渠道:mailbox,从另一个上下文中发送过来的消息(工作项)可以放在mailbox中交给actor,请参见图9-5。

图9-5 scala的actor和mailbox

要创建actor,扩展Actor类就行:

import scala.actors._class MyActor extends Actor {  def act {    ...  }}  

这看起来跟Java代码中声明Thread的子类很像。跟线程一样,我们也要告诉actor开始启动,并进入消息接收的状态,这要调用start方法。

Scala同样提供了创建actor的工厂方法actor(与Java里创建Runnable匿名实现类的静态工厂方法相对应)。用它写出来的Scala代码很精炼:

val myactor = actor {  ...}  

传给actor的代码块会变成act方法中的内容。另外,这样创建的actor不需要再单独调用start,它会自动启动。

这是一块香甜的语法糖,但我们还要介绍Scala并发模型的核心部件mailbox,所以别回味了,现在就去看看吧。

9.6.2 用mailbox跟actor通信

从另一个对象给actor发消息很简单,只要在actor对象上调用!方法就行了。

然而在接收端要有代码处理这些消息,否则它们就会堆在mailbox里。另外,actor方法体通常需要有个循环,以便能处理所有流入的消息。我们在Scala REPL中实际操练一下:

scala> import scala.actors.Actor._       val myact = actor {         while (true) {          receive {            case incoming => println(/"I got mail: /"+ incoming)          }        }      }myact: scala.actors.Actor = [email protected]scala> myact ! /"Hello!/"I got mail: Hello!scala> myact ! /"Goodbye!/"I got mail: Goodbye!scala> myact ! 34I got mail: 34  

上面代码中的receive方法就是actor对消息的处理。而工厂方法的参数(代码块)则是消息处理方法的主体。

注意 总体来说,Scala模型跟我们第4章(代码清单4-13)讨论的处理模式相似,Java处理线程相当于acctor的角色,LinkedBlockingQueue相当于Scala中的mailbox。Scala只是以非常直白的方式为这种模式提供了语言和类库层面的支持,可以大量减少使用这种模式时所要编写的套路化代码。

尽管这个例子非常简单,但也包含了很多使用actor的基础知识:

  • 在actor方法中要用循环的方式处理接收消息流;

  • receive方法处理接收到的消息;

  • 用一组case作为receive的主体。

最后这点还得继续讨论。这一组case被称为偏函数1。之所以要这样用,是因为Scala中的actor还有一点比Java方便。具体来说就是mailbox是不区分类型的。也就是说你可以向actor发送任何类型的消息,actor可以用 类型化模式和构造器模式接收不同类型的消息。

1 在Scala中,偏函数是指类型为PartialFunction[-A,+B]的函数。A是其接受的函数类型,B是其返回的结果类型。偏函数最大的特点就是它只接受其参数定义域的一个子集,而对于这个子集之外的参数则抛出运行时异常。这与case语句非常契合,因为我们在使用case语句时常常是匹配一组具体的模式,最后用“_”来代表剩余的模式。如果一组case语句没有涵盖所有的情况,那么这组case语句就可以被看做是一个偏函数。——译者注

除了这些基础知识,这里还有一些使用actor的最佳实践。编写代码应该尽量遵循下面几条规则:

  • 把传入消息做成不可变的;

  • 考虑把消息类型做成case类;

  • 不要在actor内部做阻塞操作,一个也别做。

不是每一个程序都需要遵守所有的最佳实践,但大多数应用程序应该都能从这些建议中受益。

对于更加复杂的actor,经常有必要控制它的启动和关闭。关闭actor通常都是用带有Boolean条件判断的循环。如果你喜欢,也可以将actor写成函数式的风格,这样传入的消息就不会影响它的状态。

Scala对基于actor的并发编程提供的支持还有很多。我们在这里看到的只是皮毛。如果想全面了解,请参阅Nilanjan Raychaudhuri的大作Scala in Action(Manning, 2010)。