Java并发57:Akka Actors并发框架浅谈及入门示例

本章对Actors并发框架进行初步的介绍和入门示例的演示,关于其更深层次的内容,以后会系统性的进行学习。

1.Actors并发模型简介

Actors并发模型是计算机科学领域中的一个并行计算模型,它把actors当做通用的并行计算原语。
一个actor对接收到的消息做出响应,进行本地决策,可以创建更多的actor,或者发送更多的消息;同时准备接收下一条消息。
在Actor理论中,一切都被认为是actor,这和面向对象语言里一切都被看成对象很类似。
但包括面向对象语言在内的软件通常是顺序执行的,而Actor模型本质上则是并发的。

Actors模型对并发模型进行了更高的抽象,它是一种异步、非阻塞、高性能的轻量级事件驱动编程模型。所谓轻量级事件指的是这些acotr内存消耗极小,1GB内存可容纳百万级别个Actor。Actors模型适用于可以用于高并发、分布式场景。

JDK本身并没有提供对Actors并发模型的实现,不过其他的开发库中已经实现并且被广泛应用,例如本章的学习对象:Akka

2.Akka Actors模型简介

Akka Actors官方文档地址:https://doc.akka.io/docs/akka/current/index-actors.html

Akka Actors的模型图如下:

 

 


总结:

  • Actor与Actor之前只能用消息进行通信。
  • 每个Actor都有一个邮箱。
  • Actor与Actor之间并不是直接通过消息通信,而是将消息发送至此Actor的邮箱MailBox
  • 消息在邮箱MailBox是有序的。
  • 每个Actor在处理单个消息时都是串行的。
  • Actor中的消息是不可变的。
  • 消息的传递不保证绝对可靠

Akka Actors模型的优势:

  • 轻量级事件比线程的粒度小得多,意味着可以在程序中使用大量的Actor。
  • Akka提供了一套容错机制,允许在出错时进行一些恢复或者重置操作。
  • Akka依靠其透明的远程Actor定位服务,保证了其分布式并发环境中的可用性

3.Akka Actors重要概念

下面介绍Akka Actors模型的几个重要概念:

1.Actor

Actor即角色,前面已经说过,Akka Actors模型将actors当做通用的并行计算原语,所以Actor是必不可少的。

Actor总结:

  • Akka Actor的组织结构是一种树形结。
  • 因为Actor是树形组织,所以Actor的路径类似于文件的路径。
  • 每个Actor都有父级,有可能有子级当然也可能没有。
  • 父级Actor给其子级Actor分配资源,任务,并管理其的生命状态(监管和监控)。
  • 如果我们知道一个远程Actor的具体位置,那么我们就可以向他发送消息。
  • 一个本地Actor的路径:akka://search-system/user/master
  • 一个远程Actor的路径:akka.tcp://search-system@host.example.com:5678/user/master

2.ActorSystem

ActorSystem即角色系统,为了统一的调度和管理系统中的众多actors,我们需要首先定义一个ActorSystem。

ActorSystem的主要功能有三个:

  1. 统一管理和调度actors,如:任务的拆分、处理等等。
  2. 配置Actor系统参数,如:拦截器、优先、路由策略等等。
  3. 日志功能:为了保证Akka的高容错机制,编程中尽量需要完善的日志记录,以便出错处理。

3.ActorRef

ActorRef即角色引用,每个Actor有唯一的ActorRef,Actor引用可以看成是Actor的代理,与Actor打交道都需要通过Actor引用。

4.Akka Actors入门编程流程

下面简述一下Akka Actors的入门级的编程流程:

  1. 编写Message类,用于存放消息。
  2. 编写Actor类,用于定义接收到消息之后所做的操作,注意日志处理。
  3. 定义一个ActorSystem,用于管理和调度程序中的Actor。
  4. 定义ActorRef,用于引用Actor类。
  5. 调用actorRef.tell(message),用于向此Actor发送消息。
  6. 关闭ActorSystem。

5.编程实例

下面以两个简单的例子来展示Actor并发编程的基本流程:

  • Hello World
  • 最快搜索:通过多个搜索引擎查询某个条件,返回最快的查询结果。

5.1.Hello World

场景说明:

  • 定义一个消息类,用于存放被欢迎的内容。
  • 定义一个角色类,用于欢迎接收到的消息。

实例代码:

消息类:

/**
 * <p>Hello的消息类</p>
 * @author hanchao 2018/4/16 21:34
 **/
public class HelloMessage{
    /** 欢迎的对象 */
    private String name;

    //getter setter constructor toString
}

角色类:

/**
 * <p>Actor框架入门示例-HelloWorld的角色</p>
 *
 * @author hanchao 2018/4/16 21:07
 **/
public class HelloActor extends UntypedAbstractActor {

    //定义日志,很重要
    LoggingAdapter log = Logging.getLogger(getContext().getSystem(),this);
    /**
     * <p>重写接收方法</p>
     * @author hanchao 2018/4/16 21:31
     **/
    @Override
    public void onReceive(Object message){
        log.info("HelloActor receive message : " + message);
        //如果消息类型是HelloMessage,则进行处理
        if (message instanceof HelloMessage){
            log.info("Hello " + ((HelloMessage) message).getName() + "!");
        }
    }
}

测试类:

/**
 * <p>Actor入门示例</p>
 * @author hanchao 2018/4/16 21:37
 **/
public static void main(String[] args) {
    //创建actor系统
    ActorSystem system = ActorSystem.create("hello-system");
    //定义Actor引用
    ActorRef helloActor = system.actorOf(Props.create(HelloActor.class),"hello-actor");

    //向HelloActor发送消息
    helloActor.tell(new HelloMessage("World"),null);
    helloActor.tell(new HelloMessage("Akka Actor"),null);

    //终止Actor系统
    system.terminate();
}

运行结果:

[INFO] [04/18/2018 22:37:55.744] [hello-system-akka.actor.default-dispatcher-2] [akka://hello-system/user/hello-actor] HelloActor receive message : HelloMessage{name='World'}
[INFO] [04/18/2018 22:37:55.744] [hello-system-akka.actor.default-dispatcher-2] [akka://hello-system/user/hello-actor] Hello World!
[INFO] [04/18/2018 22:37:55.744] [hello-system-akka.actor.default-dispatcher-2] [akka://hello-system/user/hello-actor] HelloActor receive message : HelloMessage{name='Akka Actor'}
[INFO] [04/18/2018 22:37:55.744] [hello-system-akka.actor.default-dispatcher-2] [akka://hello-system/user/hello-actor] Hello Akka Actor!

5.2.最快搜索

场景说明:

通过多个搜索引擎查询某个条件,返回最快的查询结果。

  • 定义一个消息类,用于存放搜索条件。
  • 定义一个消息类,用于存放搜索结果。
  • 定义一个角色类,用于接收搜索条件、搜索和返回搜索结果。
  • 定义一个角色类,用于派发搜索任务和接收搜索结果。

实例代码:

搜索引擎工具类EngineUtils :模拟搜索引擎列表和搜索过程。

/**
 * 搜索引擎工具类
 * Created by 韩超 on 2018/3/6.
 */
public class EngineUtils {
    private final static Logger LOGGER = Logger.getLogger(EngineUtils.class);

    //搜索引擎列表
    private static List<String> engineList;

    static {
        engineList = new ArrayList<>();
        engineList.add("百度");
        engineList.add("Google");
        engineList.add("必应");
        engineList.add("搜狗");
        engineList.add("Redis");
        engineList.add("Solr");
    }

    /**
     * <p>Title: 模拟一个搜索引擎进行一次问题查询</p>
     * @author 韩超 2018/3/6 11:20
     */
    public static String searchByEngine(String question,String engine) throws InterruptedException {
        //获取随机的时间间隔
        int interval = RandomUtils.nextInt(1,5000);
//        LOGGER.info("搜索引擎[" + engine + "]正在查询,预计用时" + interval + "毫秒...");
        //当前线程休眠指定时间,模拟搜索引擎用时
        Thread.sleep(interval);
        return "通过搜索引擎[" + engine + "],首先查到关于(" + question + ")问题的结果,用时 = " + interval + "毫秒!";
    }

    public static List<String> getEngineList() {
        return engineList;
    }

    public static void setEngineList(List<String> engineList) {
        EngineUtils.engineList = engineList;
    }
}

搜索条件消息类QueryTerms ,用于存储搜索条件:

/**
* <p>Title: 定义查询条件类,用于传递消息</p>
 *
 * @author 韩超 2018/3/6 16:16
 */
static class QueryTerms {
    /**
     * 问题
     */
    private String question;
    /**
     * 搜索引擎
     */
    private String engine;

    //getter setter toString constructor
}

搜索结果消息类QueryResult ,用于存放搜索结果:

/**
* <p>Title: 定义查询结果类,用于消息传递</p>
 *
 * @author 韩超 2018/3/6 16:17
 */
static class QueryResult {
    /**
     * 查询结果
     */
    private String result;

    //getter setter toString constructor
}

搜索角色类SearchEngineAcotr ,用于接收搜索条件,调用搜索引擎进行搜索:

/**
 * <p>Title:搜索引擎Actor </br>
 * 继承UntypedAbstractActor成为一个Actor</p>
 *
 * @author 韩超 2018/3/6 14:42
 */
static class SearchEngineAcotr extends UntypedAbstractActor {
    //定义Actor日志
    private LoggingAdapter log = Logging.getLogger(getContext().getSystem(),this);
    /**
     * <p>Title: Actor都需要重写消息接收处理方法</p>
     *
     * @author 韩超 2018/3/6 14:42
     */
    @Override
    public void onReceive(Object message) throws Throwable {
        //如果消息是指定的类型Message,则进行处理,否则不处理
        if (message instanceof QueryTerms) {
            log.info("接收到搜索条件:" + ((QueryTerms) message).getQuestion());
            //通过工具类进行一次搜索引擎查询
            String result = EngineUtils.searchByEngine(((QueryTerms) message).getQuestion(), ((QueryTerms) message).getEngine());
            //通过getSender().tell(result,actor)将actor的 处理结果[result] 发送消息的发送者[getSender()]
            //通过getSender获取消息的发送方
            //通过getSelf()获取当前Actor
            getSender().tell(new QueryResult(result), getSelf());
        } else {
            unhandled(message);
        }
    }
}

主角色类QuestionQuerier ,用于分发搜索任务和接收搜索结果:

/**
* <p>Title: 问题查询器Actor</br>
 * 继承自UntypedAbstractActor</p>
 *
 * @author 韩超 2018/3/6 16:31
 */
static class QuestionQuerier extends UntypedAbstractActor {
    //定义Actor日志
    private LoggingAdapter log = Logging.getLogger(getContext().getSystem(),this);
    /**
     * 搜索引擎列表
     */
    private List<String> engines;
    /**
     * 搜索结果
     */
    private AtomicReference<String> result;
    /**
     * 问题
     */
    private String question;

    public QuestionQuerier(String question, List<String> engines, AtomicReference<String> result) {
        this.question = question;
        this.engines = engines;
        this.result = result;
    }

    /**
     * <p>Title: Actor都需要重写消息接收处理方法</p>
     *
     * @author 韩超 2018/3/6 16:35
     */
    @Override
    public void onReceive(Object message) throws Throwable {
        //如果收到查询结果,则对查询结果进行处理
        if (message instanceof QueryResult) {//如果消息是指定的类型Result,则进行处理,否则不处理
            log.info("接收到搜索结果:" + ((QueryResult) message).getResult());
            //通过CAS设置原子引用的值
            result.compareAndSet(null, ((QueryResult) message).getResult());
            //如果已经查询到了结果,则停止Actor
            //通过getContext()获取ActorSystem的上下文环境
            //通过getContext().stop(self())停止当前Actor
            getContext().stop(self());
        } else {//如果没有收到处理结果,则创建搜索引擎Actor进行查询
            log.info("开始创建搜索引擎进行查询");

            //使用原子变量去测试Actor的创建是否有序
            AtomicInteger count = new AtomicInteger(1);

            //针对每一个搜索引擎,都创建一个Actor
            for (String engine : engines) {
                log.info("为" + engine + "创建第" + count + "个搜索引擎Actor....");
                count.getAndIncrement();

                //通过actorOf(Props,name)创建Actor
                //通过Props.create(Actor.class)创建Props
                ActorRef fetcher = this.getContext().actorOf(Props.create(SearchEngineAcotr.class), "fetcher-" + engine.hashCode());
                //创建查询条件
                QueryTerms msg = new QueryTerms(question, engine);
                //将查询条件告诉Actor
                fetcher.tell(msg, self());
            }
        }
    }
}

测试代码:

/**
 * <p>Title:通过多个搜索引擎查询多个条件,并返回第一条查询结果 </p>
 *
 * @author 韩超 2018/3/6 14:15
 */
public static void main(String[] args) {
    //通过工具类获取搜索引擎列表
    List<String> engines = EngineUtils.getEngineList();
    //通过 Actor 进行并发查询,获取最先查到的答案
    String result = new FlavorActorDemo().getFirstResult("今天你吃了吗?", engines);
    //打印结果
}

/**
 * 通过多个搜索引擎查询,并返回第一条查询结果
 *
 * @param question 查询问题
 * @param engines  查询条件数组
 * @return 最先查出的结果
 * @author 韩超 2018/3/6 16:44
 */
@Override
public String getFirstResult(String question, List<String> engines) {
    //创建一个Actor系统
    ActorSystem system = ActorSystem.create("search-system");
    //创建一个原子引用用于保存查询结果
    AtomicReference<String> result = new AtomicReference<>();
    //通过静态方法,调用Props的构造器,创建Props对象
    Props props = Props.create(QuestionQuerier.class, question, engines, result);
    //通过system.actorOf(props,name)创建一个 问题查询器Actor
    final ActorRef querier = system.actorOf(props, "master");
    //告诉问题查询器开始查询
    querier.tell(new Object(), ActorRef.noSender());

    //通过while无限循环 等待actor进行查询,知道产生结果
    while (null == result.get()) ;
    //关闭 Actor系统
    system.terminate();
    //返回结果
    return result.get();
}

运行结果(某一次):

[INFO] [04/18/2018 23:10:11.949] [search-system-akka.actor.default-dispatcher-7] [akka://search-system/user/master] 开始创建搜索引擎进行查询
[INFO] [04/18/2018 23:10:11.959] [search-system-akka.actor.default-dispatcher-7] [akka://search-system/user/master] 为百度创建第1个搜索引擎Actor....
[INFO] [04/18/2018 23:10:11.959] [search-system-akka.actor.default-dispatcher-7] [akka://search-system/user/master] 为Google创建第2个搜索引擎Actor....
[INFO] [04/18/2018 23:10:11.959] [search-system-akka.actor.default-dispatcher-7] [akka://search-system/user/master] 为必应创建第3个搜索引擎Actor....
[INFO] [04/18/2018 23:10:11.959] [search-system-akka.actor.default-dispatcher-6] [akka://search-system/user/master/fetcher-964584] 接收到搜索条件:今天你吃了吗?
[INFO] [04/18/2018 23:10:11.959] [search-system-akka.actor.default-dispatcher-7] [akka://search-system/user/master] 为搜狗创建第4个搜索引擎Actor....
[INFO] [04/18/2018 23:10:11.959] [search-system-akka.actor.default-dispatcher-7] [akka://search-system/user/master] 为Redis创建第5个搜索引擎Actor....
[INFO] [04/18/2018 23:10:11.959] [search-system-akka.actor.default-dispatcher-7] [akka://search-system/user/master] 为Solr创建第6个搜索引擎Actor....
[INFO] [04/18/2018 23:10:11.969] [search-system-akka.actor.default-dispatcher-7] [akka://search-system/user/master/fetcher-2138589785] 接收到搜索条件:今天你吃了吗?
[INFO] [04/18/2018 23:10:11.969] [search-system-akka.actor.default-dispatcher-2] [akka://search-system/user/master/fetcher-2582786] 接收到搜索条件:今天你吃了吗?
[INFO] [04/18/2018 23:10:11.969] [search-system-akka.actor.default-dispatcher-5] [akka://search-system/user/master/fetcher-784239] 接收到搜索条件:今天你吃了吗?
[INFO] [04/18/2018 23:10:11.969] [search-system-akka.actor.default-dispatcher-3] [akka://search-system/user/master/fetcher-78837083] 接收到搜索条件:今天你吃了吗?
[INFO] [04/18/2018 23:10:11.975] [search-system-akka.actor.default-dispatcher-4] [akka://search-system/user/master/fetcher-823867] 接收到搜索条件:今天你吃了吗?
[INFO] [04/18/2018 23:10:13.255] [search-system-akka.actor.default-dispatcher-11] [akka://search-system/user/master] 接收到搜索结果:通过搜索引擎[搜狗],首先查到关于(今天你吃了吗?)问题的结果,用时 = 1279毫秒!
[INFO] [04/18/2018 23:10:15.504] [search-system-akka.actor.default-dispatcher-5] [akka://search-system/user/master] Message [pers.hanchao.flavors.FlavorActorDemo$QueryResult] from Actor[akka://search-system/user/master/fetcher-2582786#-286481483] to Actor[akka://search-system/user/master#-1184484885] was not delivered. [1] dead letters encountered. This logging can be turned off or adjusted with configuration settings 'akka.log-dead-letters' and 'akka.log-dead-letters-during-shutdown'.
[INFO] [04/18/2018 23:10:15.504] [search-system-akka.actor.default-dispatcher-5] [akka://search-system/user/master] Message [pers.hanchao.flavors.FlavorActorDemo$QueryResult] from Actor[akka://search-system/user/master/fetcher-964584#166806804] to Actor[akka://search-system/user/master#-1184484885] was not delivered. [2] dead letters encountered. This logging can be turned off or adjusted with configuration settings 'akka.log-dead-letters' and 'akka.log-dead-letters-during-shutdown'.
[INFO] [04/18/2018 23:10:15.504] [search-system-akka.actor.default-dispatcher-5] [akka://search-system/user/master] Message [pers.hanchao.flavors.FlavorActorDemo$QueryResult] from Actor[akka://search-system/user/master/fetcher-2138589785#376820931] to Actor[akka://search-system/user/master#-1184484885] was not delivered. [3] dead letters encountered. This logging can be turned off or adjusted with configuration settings 'akka.log-dead-letters' and 'akka.log-dead-letters-during-shutdown'.
[INFO] [04/18/2018 23:10:15.504] [search-system-akka.actor.default-dispatcher-5] [akka://search-system/user/master] Message [pers.hanchao.flavors.FlavorActorDemo$QueryResult] from Actor[akka://search-system/user/master/fetcher-78837083#-224726121] to Actor[akka://search-system/user/master#-1184484885] was not delivered. [4] dead letters encountered. This logging can be turned off or adjusted with configuration settings 'akka.log-dead-letters' and 'akka.log-dead-letters-during-shutdown'.
[INFO] [04/18/2018 23:10:15.504] [search-system-akka.actor.default-dispatcher-5] [akka://search-system/user/master] Message [pers.hanchao.flavors.FlavorActorDemo$QueryResult] from Actor[akka://search-system/user/master/fetcher-784239#1622650486] to Actor[akka://search-system/user/master#-1184484885] was not delivered. [5] dead letters encountered. This logging can be turned off or adjusted with configuration settings 'akka.log-dead-letters' and 'akka.log-dead-letters-during-shutdown'.

参考文献

[1] Akka系列(一):Akka简介与Actor模型
[2] 漫谈并发编程:Actor模型
[3] Java并发的四种风味:Thread、Executor、ForkJoin和Actor



posted @ 2021-10-23 15:48  姚春辉  阅读(1436)  评论(0编辑  收藏  举报