1. 介绍

Akka 是一个Java/Scala库,使用 Actor 模型轻松开发高并发、分布式应用

在本教程中,我们将学习如何创建Actor、实现消息通信、如何停止Actor等基本功能。最后,我们还将记录使用 Akka 时的一些最佳实践。

2. Actor 模型

The Actor Model isn’t new to the computer science community. It was first introduced by Carl Eddie Hewitt in 1973, as a theoretical model for handling concurrent computation.

It started to show its practical applicability when the software industry started to realize the pitfalls of implementing concurrent and distributed applications.

An actor represents an independent computation unit. Some important characteristics are:

  • an actor encapsulates its state and part of the application logic
  • actors interact only through asynchronous messages and never through direct method calls
  • each actor has a unique address and a mailbox in which other actors can deliver messages
  • the actor will process all the messages in the mailbox in sequential order (the default implementation of the mailbox being a FIFO queue)
  • the actor system is organized in a tree-like hierarchy
  • an actor can create other actors, can send messages to any other actor and stop itself or any actor is has created

2.1. 优势

Developing concurrent application is difficult because we need to deal with synchronization, locks and shared memory. By using Akka actors we can easily write asynchronous code without the need for locks and synchronization.

One of the advantages of using message instead of method calls is that the sender thread won’t block to wait for a return value when it sends a message to another actor. The receiving actor will respond with the result by sending a reply message to the sender.

Another great benefit of using messages is that we don’t have to worry about synchronization in a multi-threaded environment. This is because of the fact that all the messages are processed sequentially.

Another advantage of the Akka actor model is error handling. By organizing the actors in a hierarchy, each actor can notify its parent of the failure, so it can act accordingly. The parent actor can decide to stop or restart the child actors.

3. Maven 依赖

To take advantage of the Akka actors we need to add the following dependency from Maven Central:

<dependency>
    <groupId>com.typesafe.akka</groupId>
    <artifactId>akka-actor_2.12</artifactId>
    <version>2.5.11</version>
</dependency>

4. 创建 Actor

As mentioned, the actors are defined in a hierarchy system. All the actors that share a common configuration will be defined by an ActorSystem.

For now, we’ll simply define an ActorSystem with the default configuration and a custom name:

ActorSystem system = ActorSystem.create("test-system");

Even though we haven’t created any actors yet, the system will already contain 3 main actors:

  • the root guardian actor having the address “/” which as the name states represent the root of the actor system hierarchy
  • the user guardian actor having the address “/user”. This will be the parent of all the actor we define
  • the system guardian actor having the address “/system”. This will be the parent for all the actors defined internally by the Akka system

Any Akka actor will extend the AbstractActor abstract class and implement the createReceive() method for handling the incoming messages from other actors:

public class MyActor extends AbstractActor {
    public Receive createReceive() {
        return receiveBuilder().build();
    }
}

This is the most basic actor we can create. It can receive messages from other actors and will discard them because no matching message patterns are defined in the ReceiveBuilder. We’ll talk about message pattern matching later on in this article.

Now that we’ve created our first actor we should include it in the ActorSystem:

ActorRef readingActorRef 
  = system.actorOf(Props.create(MyActor.class), "my-actor");

4.1. Actor 配置

The Props class contains the actor configuration. We can configure things like the dispatcher, the mailbox or deployment configuration. This class is immutable, thus thread-safe, so it can be shared when creating new actors.

It’s highly recommended and considered a best-practice to define the factory methods inside the actor object that will handle the creation of the Props object.

To exemplify, let’s define an actor the will do some text processing. The actor will receive a String object on which it’ll do the processing:

public class ReadingActor extends AbstractActor {
    private String text;

    public static Props props(String text) {
        return Props.create(ReadingActor.class, text);
    }
    // ...
}

Now, to create an instance of this type of actor we just use the props() factory method to pass the String argument to the constructor:

ActorRef readingActorRef = system.actorOf(
  ReadingActor.props(TEXT), "readingActor");

Now that we know how to define an actor, let’s see how they communicate inside the actor system.

5. Actor 消息

To interact with each other, the actors can send and receive messages from any other actor in the system. These messages can be any type of object with the condition that it’s immutable.

It’s a best practice to define the messages inside the actor class. This helps to write code that is easy to understand and know what messages an actor can handle.

5.1. 发送消息

Inside the Akka actor system messages are sent using methods:

  • tell()
  • ask()
  • forward()

When we want to send a message and don’t expect a response, we can use the tell() method. This is the most efficient method from a performance perspective:

readingActorRef.tell(new ReadingActor.ReadLines(), ActorRef.noSender());

The first parameter represents the message we send to the actor address readingActorRef.

The second parameter specifies who the sender is. This is useful when the actor receiving the message needs to send a response to an actor other than the sender (for example the parent of the sending actor).

Usually, we can set the second parameter to null or ActorRef.noSender(), because we don’t expect a reply. When we need a response back from an actor, we can use the ask() method:

CompletableFuture<Object> future = ask(wordCounterActorRef, 
  new WordCounterActor.CountWords(line), 1000).toCompletableFuture();

When asking for a response from an actor a CompletionStage object is returned, so the processing remains non-blocking.

A very important fact that we must pay attention to is error handling insider the actor which will respond. To return a Future object that will contain the exception we must send a Status.Failure message to the sender actor.

This is not done automatically when an actor throws an exception while processing a message and the ask() call will timeout and no reference to the exception will be seen in the logs:

@Override
public Receive createReceive() {
    return receiveBuilder()
      .match(CountWords.class, r -> {
          try {
              int numberOfWords = countWordsFromLine(r.line);
              getSender().tell(numberOfWords, getSelf());
          } catch (Exception ex) {
              getSender().tell(
               new akka.actor.Status.Failure(ex), getSelf());
               throw ex;
          }
    }).build();
}

We also have the forward() method which is similar to tell(). The difference is that the original sender of the message is kept when sending the message, so the actor forwarding the message only acts as an intermediary actor:

printerActorRef.forward(
  new PrinterActor.PrintFinalResult(totalNumberOfWords), getContext());

5.2. 接受消息

Each actor will implement the createReceive() method, which handles all incoming messages. The receiveBuilder() acts like a switch statement, trying to match the received message to the type of messages defined:

public Receive createReceive() {
    return receiveBuilder().matchEquals("printit", p -> {
        System.out.println("The address of this actor is: " + getSelf());
    }).build();
}

When received, a message is put into a FIFO queue, so the messages are handled sequentially.

6. Kill 掉 Actor

When we finished using an actor we can stop it by calling the stop() method from the ActorRefFactory interface:

system.stop(myActorRef);

We can use this method to terminate any child actor or the actor itself. It’s important to note stopping is done asynchronously and that the current message processing will finish before the actor is terminated. No more incoming messages will be accepted in the actor mailbox.

By stopping a parent actor, we’ll also send a kill signal to all of the child actors that were spawned by it.

When we don’t need the actor system anymore, we can terminate it to free up all the resources and prevent any memory leaks:

Future<Terminated> terminateResponse = system.terminate();

This will stop the system guardian actors, hence all the actors defined in this Akka system.

We could also send a PoisonPill message to any actor that we want to kill:

myActorRef.tell(PoisonPill.getInstance(), ActorRef.noSender());

The PoisonPill message will be received by the actor like any other message and put into the queue. The actor will process all the messages until it gets to the PoisonPill one. Only then the actor will begin the termination process.

Another special message used for killing an actor is the Kill message. Unlike the PoisonPill, the actor will throw an ActorKilledException when processing this message:

myActorRef.tell(Kill.getInstance(), ActorRef.noSender());

7. 总结

In this article, we presented the basics of the Akka framework. We showed how to define actors, how they communicate with each other and how to terminate them.

We’ll conclude with some best practices when working with Akka:

  • use tell() instead of ask() when performance is a concern
  • when using ask() we should always handle exceptions by sending a Failure message
  • actors should not share any mutable state
  • an actor shouldn’t be declared within another actor
  • actors aren’t stopped automatically when they are no longer referenced. We must explicitly destroy an actor when we don’t need it anymore to prevent memory leaks
  • messages used by actors should always be immutable

As always, the source code for the article is available over on GitHub.