Bootstrap

Akka编程入门

注重版权,若要转载烦请附上作者和链接

作者:Joshua_yi

链接:https://blog.csdn.net/weixin_44984664/article/details/124732703


一、Akka介绍

Akka 一词据说来源于瑞典的一座山,我们说到 Akka时,通常是指一个分布式工具集,用于协调远程计算资源来进行一些工作。Akka 是 Actor并发模型的一种现代化实现。现在的 Akka 可以认为是从许多其他技术发展演化而来的,它借鉴了 Erlang 的 Actor 模型实现,同时又引入了许多新特性,帮助构建能够处理如今大规模问题的应用程序。——《Akka入门与实践》,Jason GoodWin

Akka 是一个用于在 JVM 上构建高并发、分布式和可容错的事件驱动应用程序的运行时工具包。Akka 既可以用于 Java,也可以用于 Scala。
Akka 提供:

  • 不使用原子或锁之类的低级并发构造的多线程行为,甚至可以避免你考虑内存可见性问题。
  • 系统及其组件之间的透明远程通信,使你不再编写和维护困难的网络代码。
  • 一个集群的、高可用的体系结构,具有弹性、可按需扩展性,使你能够提供真正的反应式系统。

应用
目前 Akka 已经在多家互联网&软件公司广泛使用,比如 eBay、Amazon、VMWare、PayPal、阿里、惠普、豌豆荚等,涉及行业包括游戏、金融投资、医疗保健、数据分析等。
使用场景包括:

  • 服务后端:比如 rest web,websocket 服务,分布式消息处理等。
  • 并发&并行:比如日志异步处理,密集数据计算等。
  • 对高并发和密集计算的系统,Akka 都是适用的

相关资料

二、Actor模型

(一)基础概念

Actor模型是并发计算的理论模型,Akka的核心是Actor模型的一种实现,Actors 是 Akka 的执行单元。这些设计让编写正确的并发、并行和分布式系统更加容易。
Actor是一个并发原语,可以理解为一个工人,能够处理任务的进程和线程。每个实例的开销只有大约 300 字节。(1G 内存可容纳接近 300 万个 Actor)
Actor 每次只同步处理一个消息。邮箱本质上是等待 Actor 处理的一个工作队列。
在这里插入图片描述
概念

  • Actor:一个表示工作节点的并发原语,同步处理接收到的消息。Actor 可以保存并修改内部状态。
  • 消息:用于跨进程(比如多个 Actor 之间)通信的数据。
  • 消息传递:一种软件开发范式,通过传递消息来触发各种行为,而不是直接触发行为。
  • 邮箱地址:消息传递的目标地址,当 Actor 空闲时会从该地址获取消息进行处理。
  • 邮箱:在 Actor 处理消息前具体存储消息的地方。可以将其看作是一个消息队列。
  • Actor 系统:多个 Actor 的集合以及这些 Actor 的邮箱地址、邮箱和配置等。

(二)Actor 模型如何满足现代分布式系统的需求?(非阻塞保证)

常见的编程实践不能合适地满足现代系统的需求。Actor 模型以一种原则性的方式解决了这些缺点,允许系统以更好地匹配我们的构思模型的方式运行。Actor 模型抽象允许你从通信的角度来考虑你的代码。

1、等待自由

如果保证每个调用都以有限的步骤完成,则方法是无等待的,即等待自由(wait-freedom)。如果一个方法是有界“无等待”的,那么步骤的数量有一个有限的上限。
根据这个定义,无等待方法永远不会被阻塞,因此不会发生死锁。此外,由于每个 Actor 都可以在有限的步骤之后(调用完成时)继续进行,因此无等待方法没有饥饿。

2、锁自由

锁自由(lock-freedom)比等待自由更弱。在无锁调用的情况下,某些方法以有限的步数完成可能导致无限的等待。这个定义意味着没有死锁的调用是不可能的。另一方面,某些调用以有限的步骤完成的保证不足以保证所有调用最终都完成。换句话说,锁自由不足以保证不发生饥饿。

3、障碍自由

障碍自由(obstruction-freedom)是本文讨论的最薄弱的非阻塞保证。如果一个方法在某个时间点之后独立执行(其他线程不执行任何步骤,例如:挂起),则该方法称为无障碍的,它以有限的步骤完成。所有锁自由对象都是无障碍的,但相反的情况通常是不正确的。
乐观并发控制(OCC)方法通常是无障碍的。OCC 方法是,每个 Actor 都试图在共享对象上执行其操作,但如果 Actor 检测到来自其他对象的冲突,则会回滚修改,并根据某些计划重试。如果有一个时间点,其中一个 Actor 是唯一的尝试者,那么操作将成功。

三、Actor

Actor是一个并发原语,可以理解为一个工人,能够处理任务的进程和线程。每个实例的开销只有大约 300 字节。Actor 和对象的不同之处在于其不能被直接读取、修改或是调用。反之,Actor 只能
通过消息传递的方式与外界进行通信。消息传递是异步的。无论是处理消息还是回复消息,Actor 对外界都没有依赖。
Actor 是封装状态和行为的对象( 是状态、行为、邮箱、子 Actor 和监督者策略的容器)。它们通过交换放在收件人邮箱中的消息进行专门的通信。从某种意义上说,Actor 是面向对象编程最严格的形式,但最好将其视为“人”:当与 Actor 一起建模解决方案时,设想一组人员并为其分配子任务,将其功能安排到组织结构中,并考虑如何升级失败。
Actor 系统的典型特征是,任务被拆分和委托,直到它们变得足够小,可以一块处理。这样做,不仅任务本身结构清晰,而且结果 Actor 可以根据他们应该处理哪些消息、应该如何正常反应以及应该如何处理失败来进行推理。如果一个 Actor 没有处理特定情况的方法,它会向其监督 Actor 发送相应的失败消息,请求帮助。然后,递归结构允许在正确的级别处理故障。

  • Actor 每次只同步处理一个消息。
  • 每个Actor的创建者就是他的监督者
  • Actor 有一个明确的生命周期,当不再被引用时它们不会被自动销毁
  • 控制当 Actor 终止时如何释放资源
    使用 Actor 允许我们:
  • 在不使用锁的情况下强制封装。
  • 利用协同实体对信号作出反应、改变状态、相互发送信号的模型来驱动整个应用程序向前发展。
  • 不要担心执行机制与我们的世界观不匹配。

(一)组件介绍

  • Actor引用:为了从 Actor 模型中获益,需要将 Actor 对象从外部屏蔽。因此,使用 Actor 引用将 Actor 表示为外部对象
  • 状态:Actor 对象通常包含一些反映 Actor 可能处于的状态的变量。这可以是一个显式状态机(例如,使用「FSM」模块),也可以是一个计数器、一组监听器、挂起的请求等。
  • 行为:每次处理消息时,它都与 Actor 的当前行为相匹配。行为(Behavior)指的是一个函数,它定义了在该时间点对消息做出反应时要采取的操作。在构造 Actor 对象期间定义的初始行为是特殊的,因为重新启动 Actor 会将其行为重置为初始行为。
  • 邮箱:Actor 的目的是处理消息,这些消息是从其他 Actor(或从 Actor 系统外部)发送给 Actor 的。连接发送方和接收方的部分是 Actor 的邮箱:每个 Actor 只有一个邮箱,所有发送方都将其消息排队。有不同的邮箱实现可供选择,默认为FIFO
  • 子Actor:每个 Actor 都可能是一个监督者:如果它为分配子任务创建子 Actor,它将自动对它们进行监督。
  • Actor终止:一旦一个 Actor 终止,即以一种不被重启处理的方式失败、自行停止或被其监督者停止,它将释放其资源,将其邮箱中的所有剩余邮件排入系统的“死信邮箱”,该邮箱将它们作为死信(DeadLetters)转发到事件流(EventStream)。然后在 Actor 引用中用系统邮箱替换原 Actor 的邮箱,将所有新消息作为死信重定向到事件流。在监管时,我们需要根据不同的情况选择不同的处理方案(比如停止、重启、恢复或者失败上溯)和策略(比如 1 vs 1、1 vs N 策略)。

(二)Akka的Actor层级

Akka 的 Actor 总是属于父 Actor。通常,你可以通过调用getContext().actorOf()来创建 Actor。与创建一个“独立的” Actor 不同,这会将新 Actor 作为一个子节点注入到已经存在的树中:创建 Actor 的 Actor 成为新创建的子 Actor 的父级。
所有的 Actor 都有一个共同的父节点,即用户守护者。可以使用system.actorOf()在当前 Actor 下创建新的 Actor 实例。
在这里插入图片描述

事实上,在代码中创建 Actor 之前,Akka 已经在系统中创建了三个 Actor 。这些内置的 Actor 的名字包含guardian,因为他们守护他们所在路径下的每一个子 Actor。守护者 Actor 包括:

  • /,根守护者(root guardian)。这是系统中所有 Actor 的父 Actor,也是系统本身终止时要停止的最后一个 Actor。
  • /user,守护者(guardian)。这是用户创建的所有 Actor 的父 Actor。不要让用户名混淆,它与最终用户和用户处理无关。使用 Akka 库创建的每个 Actor 都将有一个事先准备的固定路径/user/。
  • /system,系统守护者(system guardian)。这是除上述三个 Actor 外,系统创建的所有 Actor 的父 Actor,

(三)Actors 和 Java 内存模型

通过 Akka 中的 Actor 实现,多个线程可以通过两种方式在共享内存上执行操作:

  • 如果消息发送给某个 Actor(例如由另一个 Actor)。在大多数情况下,消息是不可变的,但是如果该消息不是正确构造的不可变对象,没有“先于发生”规则,则接收者可能会看到部分初始化的数据结构。
  • 如果 Actor 在处理消息时更改其内部状态,并在稍后处理另一条消息时访问该状态。重要的是要认识到,对于 Actor 模型,你不能保证同一线程将对不同的消息执行相同的 Actor。
    为了防止 Actor 出现可见性和重新排序问题,Akka 保证以下两条“先于发生”规则:
  • Actor 发送规则:向 Actor 发送消息的过程发生在同一个 Actor 接收消息之前。
  • Actor 后续处理规则:一条消息的处理发生在同一个 Actor 处理下一条消息之前。
    注释:在外行术语中,这意味着当 Actor 处理下一条消息时,Actor 内部字段的更改是可见的。因此,Actor 中的字段不必是volatile或equivalent的。
    这两个规则仅适用于同一个 Actor 实例,如果使用不同的 Actor,则这两个规则无效。
  • 为了避免共享可变状态陷阱,消息应该是不可变的。

(三)Actor生命周期

Actor 在运行时中会经历不同的阶段,比如创建、运行、重启和销毁等,这一系列的过程或状态,我们称之为生命周期。在理想情况下,Actor 应该像一头老黄牛一样任劳任怨地不停工作,但实际情况往往是,当某个不算常态的错误发生时(比如网络偶尔超时),我们想让它重启一下,然后重复之前的动作;又或者,当一个程序异常被抛出时,我们不得不停止该 Actor,让它不能继续走下去,这些行为有可能是手动触发的,也可能是 Actor 的监督-容错机制导致的。无论怎样,当这些行为发生时,我们都希望能及时感知并做有效处理。
Actor 生命周期主要包括启动(Start)、恢复(Resume)、重启(Restart)、停止(Stop)这四个阶段,每个阶段都会伴随着自身状态信息的变化。
在这里插入图片描述

1、创建并启动

当通过 actorOf 创建并启动 Actor 时,该 Actor 除了默认拥有已指定的 path 外,也会被分配一个 UID(可以通过 getSelf().path().uid()得到该值),作为它的唯一标识。在 Actor 启动后,会默认调用preStart 方法,在该方法里,我们可以做些资源初始化的操作。

2、恢复运行

当 Actor 出现某种异常后,通过容错机制,可以让该 Actor 恢复并继续运行,此时 Actor 会延用之前的实例,状态也会被保留下来。

3、重启

当 Actor 出现某种异常后,通过容错机制,可以让该 Actor 执行重启。重启的具体过程如下:
调用旧实例的preRestart 方法,该方法默认情况下会停掉所有子级 Actor,并调用postStop 方法;
创建新实例,并在新实例上调用postRestart 方法,该方法默认情况下会调用preStart 方法。
Actor 重启后,path 和 UID 不变,这意味着假如要继续使用该 Actor,不需要重新获取ActorRef 对象,使用之前那个就可以了。同时也得注意到:Actor 重启后不会保留自身状态。

4、停止

当 Actor 停止时,会调用postStop 方法,同时会发送一条Terminated信息给自己的监控者,告知自己已处于停止状态。

(四)Actor 最佳实践

  1. Actor 应该像好的同事一样:高效地工作,而不是不必要地打扰其他人,并且避免占用资源。翻译成编程语言,这意味着以事件驱动的方式处理事件并生成响应(或更多请求)。Actor 不应在可能是锁、网络套接字等外部实体上阻塞,即占用线程时被动等待,除非这是不可避免的。
  2. 不要在 Actor 之间传递可变对象。为了确保这一点,最好选择不可变的消息。如果通过将它们的可变状态暴露到外部来破坏 Actor 的封装,则会返回正常的 Java 并发域,并存在所有的缺点。
  3. Actor 被设计成行为和状态的容器,接受这一点意味着不经常在消息中发送行为。其中一个风险是不小心在 Actor 之间共享可变状态,而这种对 Actor 模型的违反不幸地破坏了所有属性。
  4. 顶级 Actor 是错误内核的最核心部分,因此要谨慎地创建它们,并且更倾向于真正的分层系统。这对于故障处理(同时考虑配置的粒度和性能)有好处,而且它还减少了对守护者 Actor 的压力,如果使用过度,这是一个单一的竞争点。

(五)Actor 使用

actor常见API操作
https://github.com/guobinhit/akka-guide/blob/master/articles/actors/actors.md

1、创建

Actor 类是通过继承AbstractActor类并在createReceive方法中设置“初始行为”来实现的。
createReceive方法没有参数,并返回AbstractActor.Receive。它定义了 Actor 可以处理哪些消息,以及如何处理消息的实现。可以使用名为ReceiveBuilder的生成器来构建此类行为。在AbstractActor中,有一个名为receiveBuilder的方便的工厂方法。

Props是一个配置类,用于指定创建 Actor 的选项,将其视为不可变的,因此可以自由共享用于创建 Actor 的方法,包括关联的部署信息(例如,要使用哪个调度程序,请参阅下面的更多内容)。

static class DemoActor extends AbstractActor {
  /**
   * Create Props for an actor of this type.
   *
   * @param magicNumber The magic number to be passed to this actor’s constructor.
   * @return a Props for creating this actor, which can then be further configured (e.g. calling
   *     `.withDispatcher()` on it)
   */
  static Props props(Integer magicNumber) {
    // You need to specify the actual type of the returned actor
    // since Java 8 lambdas have some runtime type information erased
    return Props.create(DemoActor.class, () -> new DemoActor(magicNumber));
  }

  private final Integer magicNumber;

  public DemoActor(Integer magicNumber) {
    this.magicNumber = magicNumber;
  }

  @Override
  public Receive createReceive() {
    return receiveBuilder()
        .match(
            Integer.class,
            i -> {
              getSender().tell(i + magicNumber, getSelf());
            })
        .build();
  }
}

static class SomeOtherActor extends AbstractActor {
  // Props(new DemoActor(42)) would not be safe
  ActorRef demoActor = getContext().actorOf(DemoActor.props(42), "demo");
  // ...
}

AbstractActor它还提供:

  • getSelf(),对 Actor 的ActorRef的引用
  • getSender(),前一次接收到的消息的发送方 Actor 的引用
  • supervisorStrategy(),用户可重写定义用于监视子 Actor 的策略
    该策略通常在 Actor 内部声明,以便访问决策函数中 Actor 的内部状态:由于故障作为消息发送给其监督者并像其他消息一样进行处理(尽管不属于正常行为),因此 Actor 内的所有值和变量都可用,就像sender引用一样(报告失败的是直接子级;如果原始失败发生在一个遥远的后代中,则每次仍向上一级报告)。
  • getContext()公开 Actor 和当前消息的上下文信息,例如:
    • 创建子 Actor 的工厂方法(actorOf)
    • Actor 所属的系统
    • 父级监督者
    • 受监督的子级
    • 生命周期监控
    • 如Become/Unbecome中所述的热交换行为栈

2、发送消息

消息通过以下方法之一发送给 Actor。

  • tell的意思是“发送并忘记(fire-and-forget)”,例如异步发送消息并立即返回。
  • ask异步发送消息,并返回一个表示可能的答复。
    每一个发送者都有消息顺序的保证。

使用ask会带来性能方面的影响,因为有些东西需要跟踪它何时超时,需要有一些东西将一个Promise连接到ActorRef中,并且还需要通过远程处理实现它。所以,我们更倾向于使用tell,只有当你有足够的理由时才应该使用ask。
在所有这些方法中,你可以选择传递自己的ActorRef。将其作为一种实践,因为这样做将允许接收者 Actor 能够响应你的消息,因为发送者引用与消息一起发送。

Tell: Fire-forget
这是发送消息的首选方式,它不用等待消息返回,因此不是阻塞的。这提供了最佳的并发性和可伸缩性的特性。

// don’t forget to think about who is the sender (2nd argument)
target.tell(message, getSelf());

发送方引用与消息一起传递,并在处理此消息时通过getSender()方法在接收 Actor 中使用。在一个 Actor 内部,通常是getSelf()作为发送者,但在某些情况下,回复(replies)应该路由到另一个 Actor,例如,父对象,其中tell的第二个参数将是另一个 Actor。在 Actor 外部,如果不需要回复,则第二个参数可以为null;如果在 Actor 外部需要回复,则可以使用下面描述的ask模式。
Ask: Send-And-Receive-Future
ask模式涉及 Actor 和Future,因此它是作为一种使用模式而不是ActorRef上的一种方法提供的:

import static akka.pattern.Patterns.ask;
import static akka.pattern.Patterns.pipe;

import java.util.concurrent.CompletableFuture;
final Duration t = Duration.ofSeconds(5);

// using 1000ms timeout
CompletableFuture<Object> future1 =
    ask(actorA, "request", Duration.ofMillis(1000)).toCompletableFuture();

// using timeout from above
CompletableFuture<Object> future2 = ask(actorB, "another request", t).toCompletableFuture();

CompletableFuture<Result> transformed =
    CompletableFuture.allOf(future1, future2)
        .thenApply(
            v -> {
              String x = (String) future1.join();
              String s = (String) future2.join();
              return new Result(x, s);
            });

pipe(transformed, system.dispatcher()).to(actorC);

这个例子演示了ask和pipeTo模式在Future上的结合,因为这可能是一个常见的组合。请注意,以上所有内容都是完全非阻塞和异步的:ask生成一个,其中两个使用CompletableFuture.allOf和thenApply方法组合成新的Future,然后pipe在CompletionStage上安装一个处理程序,以将聚合的Result提交给另一个 Actor。
使用ask会像使用tell一样向接收 Actor 发送消息,并且接收 Actor 必须使用getSender().tell(reply, getSelf())才能完成返回的值。ask操作涉及创建一个用于处理此回复的内部 Actor,该 Actor 需要有一个超时,在该超时之后才能将其销毁,以便不泄漏资源;

  • 要完成带异常的,你需要向发件人发送akka.actor.Status.Failure消息。当 Actor 在处理消息时抛出异常,不会自动执行此操作。
try {
  String result = operation();
  getSender().tell(result, getSelf());
} catch (Exception e) {
  getSender().tell(new akka.actor.Status.Failure(e), getSelf());
  throw e;
}

如果 Actor 未完成,则它将在超时期限(指定为ask方法的参数)之后过期;这将使用AskTimeoutException完成CompletionStage。
这可以用于注册回调以便在完成时获取通知,从而提供避免阻塞的方法。

  • 当使用Future的回调时,内部 Actor 需要小心避免关闭包含 Actor 的引用,即不要从回调中调用方法或访问封闭 Actor 的可变状态。这将破坏 Actor 的封装,并可能引入同步错误和竞态条件,因为回调将被同时调度到封闭 Actor。目前还没有一种方法可以在编译时检测到这些非法访问。

3、接收消息

Actor 必须通过在AbstractActor中实现createReceive方法来定义其初始接收行为:

@Overridepublic Receive createReceive() {
  return receiveBuilder().match(String.class, 
      s -> System.out.println(s.toLowerCase()))
  .build();
}

返回类型是AbstractActor.Receive,它定义了 Actor 可以处理哪些消息,以及如何处理这些消息的实现。可以使用名为ReceiveBuilder的生成器来构建此类行为。下面是一个例子:

import akka.actor.AbstractActor;
import akka.event.Logging;
import akka.event.LoggingAdapter;

public class MyActor extends AbstractActor {
  private final LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this);

  @Override
  public Receive createReceive() {
    return receiveBuilder()
        .match(
            String.class,
            s -> {
              log.info("Received String message: {}", s);
            })
        .matchAny(o -> log.info("received unknown message"))
        .build();
  }
}

四、Akka相关库

在这里插入图片描述

  • akka-actor:Akka 的核心库是akka-actor,但是 Actor 在整个 Akka 库中使用,它提供了一个一致的、集成的模型,使你能够独立地解决并发或分布式系统设计中出现的挑战。从鸟瞰图(birds-eye)来看,Actor 是一种将 OOP 的支柱之一封装用到极致的编程范式。与对象不同,Actor 不仅封装了他们的状态,而且还封装了他们的执行。与 Actor 的通信不是通过方法调用,而是通过传递消息。虽然这种差异看起来很小,但实际上它允许我们在并发性和远程通信方面打破 OOP 的限制。
  • akka-remote:远程处理(Remoting)使存活(live)在不同计算机上的 Actor 能够无缝地交换消息。
  • akka-cluster:如果你有一组协作解决某些业务问题的 Actor 系统,那么你可能希望以规范的方式管理这些系统集(set of systems)。当远程处理解决了与远程系统组件寻址和通信的问题时,集群(Clustering)使你能够将它们组织成一个由成员协议绑定在一起的“元系统(meta-system)”。在大多数情况下,你希望使用集群模块(Cluster module)而不是直接使用远程处理。集群在远程处理之上提供了一组额外的服务,这是大多数实际应用程序所需要的。
  • akka-cluster-sharding:分片(Sharding)有助于解决在 Akka 集群成员之间分配一组 Actor 的问题。分片是一种模式,它主要与持久性(Persistence)一起使用,以将一大组需要持久化的实体(由 Actors 支持)平衡到集群的各个成员,并在成员崩溃或离开时将它们迁移到其他节点。
  • akka-cluster-singleton:分布式系统中的一个常见(事实上,有点太常见)用例是让一个实体负责一个给定的任务,该任务在集群的其他成员之间共享,并且在主机系统发生故障时进行迁移。尽管这无疑会给整个集群带来一个限制扩展的常见瓶颈,但在某些情况下,使用这种模式是不可避免的。集群单例(Cluster singleton)允许集群选择一个 Actor 系统,该系统将承载一个特定的 Actor,而其他系统始终可以独立地访问该 Actor 承担的服务。
    单例模块可用于解决这些挑战:
    • 如何确保整个集群中只有一个服务实例在运行。
    • 如何确保服务处于启动状态,即使承载它的系统在缩小规模的过程中崩溃或关闭。
    • 如何从集群的任何成员访问这个实例,假设它可以随着时间迁移到其他系统。
  • akka-persistence:就像 OOP 中的对象一样,Actor 将其状态保存在易失性内存中。一旦系统正常关闭或突然崩溃,内存中的所有数据都将丢失。持久性(Persistence)提供了使 Actor 能够持久化导致其当前状态的事件的模式。启动时,可以重播事件以恢复由 Actor 承载的实体的状态。可以查询事件流并将其输入到其他处理管道(例如外部大数据集群)或备用视图(如报表)中。
  • akka-distributed-data:在最终一致性可以接受的情况下,可以在 Akka 集群中的节点之间共享数据,甚至在集群分区面前也可以接受读和写。这可以通过使用「无冲突的复制数据类型(CRDTs)」来实现,其中不同节点上的写入可以并发进行,并随后以可预测的方式进行合并。分布式数据(Distributed Data)模块提供了共享数据和许多有用数据类型的基础结构。

五、Demo

两个Actor类型,一个是打招呼的人,一个是打印器

(一)Greeter

package com.lightbend.akka.sample;

import akka.actor.AbstractActor;
import akka.actor.ActorRef;
import akka.actor.Props;
import com.lightbend.akka.sample.Printer.Msg;

public class Greeter extends AbstractActor {
    // 实例化
    static public Props props(String message, ActorRef printerActor) {
        return Props.create(Greeter.class, () -> new Greeter(message, printerActor));
    }

    public Greeter(String name, ActorRef printerActor) {
        this.name = name;
        this.printerActor = printerActor;
    }
    // 接收的消息类型
    static public class WhoSayHello {

        public final String who;

        public WhoSayHello(String who) {
            this.who = who;
        }
    }

    private final String name;
    // 使用actor的引用来操作actor
    private final ActorRef printerActor;

    @Override
    public Receive createReceive() {
        return receiveBuilder()
            .match(WhoSayHello.class, wtg -> {
                printerActor.tell(new Msg(wtg.who + " say hello to " + name), getSelf());
            })
            .build();
    }
}

(二)Printer

package com.lightbend.akka.sample;

import akka.actor.AbstractActor;
import akka.actor.Props;
import akka.event.Logging;
import akka.event.LoggingAdapter;

public class Printer extends AbstractActor {

    // 实例化
    static public Props props() {
        return Props.create(Printer.class, () -> new Printer());
    }

    // 接收的消息
    static public class Msg {

        public final String message;

        public Msg(String message) {
            this.message = message;
        }
    }

    private LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this);

    public Printer() {
    }

    @Override
    public Receive createReceive() {
        return receiveBuilder()
            .match(Msg.class, msg -> {
                log.info(msg.message);
            })
            .build();
    }
}

(三)Main

package com.lightbend.akka.sample;

import java.io.IOException;

import com.lightbend.akka.sample.Greeter.WhoSayHello;

import akka.actor.ActorRef;
import akka.actor.ActorSystem;

public class AkkaQuickstart {
    public static void main(String[] args) {
        final ActorSystem system = ActorSystem.create("helloakka");
        try {
            //#create-actors
            final ActorRef printerActor =
                    system.actorOf(Printer.props(), "printerActor");
            final ActorRef aGreeter =
                    system.actorOf(Greeter.props("A", printerActor), "AGreeter");
            final ActorRef bGreeter =
                    system.actorOf(Greeter.props("B", printerActor), "BGreeter");
            //#create-actors
            //#main-send-messages
            aGreeter.tell(new WhoSayHello("B"), ActorRef.noSender());
            bGreeter.tell(new WhoSayHello("A"), ActorRef.noSender());
            //#main-send-messages
            System.out.println(">>> Press ENTER to exit <<<");
            System.in.read();
        } catch (IOException ioe) {
        } finally {
            system.terminate();
        }
    }
}

输出

B say hello to A
A say hello to B
;