Java并发的四种风味:Thread、Executor、ForkJoin和Actor

原文地址:Java并发的四种风味:Thread、Executor、ForkJoin和Actor

这篇文章讨论了Java应用中并行处理的多种方法。从自己管理Java线程,到各种更好的几种解决方法,Executor服务、Fork/Join 框架以及计算中的Actor模型

Java并发编程的4种风格:Threads,Executors,Fork/Join和Actors
这里写图片描述

我们生活在一个事情并行发生的世界。自然地,我们编写的程序也反映了这个特点,它们可以并发的执行。当然除了Python代码(译者注:链接里面讲述了Python的全局解释器锁,解释了原因),不过你仍然可以使用Jython在JVM上运行你的程序,来利用多处理器电脑的强大能力。

然而,并发程序的复杂程度远远超出了人类大脑的处理能力。相比较而言,我们简直弱爆了:我们生来就不是为了思考多线程程序、评估并发访问有限资源以及预测哪里会发生错误或者瓶颈。

面对这些困难,人类已经总结了不少并发计算的解决方案和模型。这些模型强调问题的不同部分,当我们实现并行计算时,可以根据问题做出不同的选择。

在这篇文章中,我将会用对同一个问题,用不同的代码来实现并发的解决方案;然后讨论这些方案有哪些好的地方,有哪些缺陷,可能会有什么样的陷阱在等着你。

我们将介绍下面几种并发处理和异步代码的方式:

  • 裸线程
  • Executors和Services
  • ForkJoin框架和并行流
  • Actor模型

为了更加有趣一些,我没有仅仅通过一些代码来说明这些方法,而是使用了一个共同的任务,因此每一节中的代码差不多都是等价的。另外,这些代码仅仅是展示用的,初始化的代码并没有写出来,并且它们也不是产品级的软件示例。


任务

任务:实现一个方法,它接收一条消息和一组字符串作为参数,这些字符串与某个搜索引擎的查询页面对应。对每个字符串,这个方法发出一个http请求来查询消息,并返回第一条可用的结果,越快越好。

如果有错误发生,抛出一个异常或者返回空都是可以的。我只是尝试避免为了等待结果而出现无限循环。

简单说明:这次我不会真正深入到多线程如何通讯的细节,或者深入到Java内存模型。如果你迫切地想了解这些,你可以看我前面的文章利用JCStress测试并发

为了后面的代码值只关注于并发编程,这里提供两个类:

  • 一个接口IFlavorDemo,用于规定并发查询方法getFirstResult
  • 一个工具类EngineUtils,用于模拟搜索引擎列表搜索方法

EngineUtils.java

/**
 * 搜索引擎工具类
 * 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;
    }
}

IFlavorDemo.java

/**
 * Created by 韩超 on 2018/3/6.
 */
public interface IFlavorDemo {
    String getFirstResult(String question, List<String > engines);
}

那么,让我们从最直接、最核心的方式来在JVM上实现并发:手动管理裸线程。

方法1:使用“原汁原味”的裸线程

解放你的代码,回归自然,使用裸线程!线程是并发最基本的单元Java线程本质上被映射到操作系统线程,并且每个线程对象对应着一个计算机底层线程

自然地,JVM管理着线程的生存期,而且只要你不需要线程间通讯,你也不需要关注线程调度。

每个线程有自己的栈空间,它占用了JVM进程空间的指定一部分。

线程的接口相当简明,你只需要提供一个Runnable,调用.start()开始计算。没有现成的API来结束线程,你需要自己来实现,通过类似boolean类型的标记来通讯。

在下面的例子中,我们对每个被查询的搜索引擎,创建了一个线程。查询的结果被设置到AtomicReference,它不需要锁或者其他机制来保证只出现一次写操作。开始吧!

代码:

/**
 * <p>并发四种口味-01 裸线程</p>
 *
 * @author hanchao 2018/3/5 21:53
 **/
public class FlavorThreadsDemo implements IFlavorDemo {
    private static final Logger LOGGER = Logger.getLogger(FlavorThreadsDemo.class);

    /**
     * 通过多个搜索引擎查询多个条件,并返回第一条查询结果
     *
     * @param question 查询问题
     * @param engines  查询条件数组
     * @return 最先查出的结果
     * @author hanchao 2018/3/5 22:05
     */
    @Override
    public String getFirstResult(String question, List<String> engines) {
        //将存放查询的数据类型设置为"Atomic"类型,保证原子性
        AtomicReference<String> result = new AtomicReference<String>();
        LOGGER.info("通过裸线程进行并发编程,自己控制现场数量:" + engines.size());

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

        //针对每一个搜索引擎,都开启一个线程进行查询
        for (String engine : engines) {
            //通过java8提供的lambda表达式创建线程
            new Thread(
                    () -> {
                        try {
                            //调用某种搜索引擎进行搜索
                            result.compareAndSet(null, EngineUtils.searchByEngine(question, engine));
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
            ).start();//通过.start()启动线程
            LOGGER.info("为搜索引擎[" + engine + "]创建" + count + "个线程...");
            count.getAndIncrement();
        }
        //无限循环,直至result有值为止
        while (result.get() == null) ;
        //返回搜索结果
        return result.get();
    }

    /**
     * <p>创建一组搜索引擎,对同一话题进行查询,并获取第一个查到的结果。</p>
     *
     * @author hanchao 2018/3/5 22:47
     **/
    public static void main(String[] args) {
        //通过工具类获取搜索引擎列表
        List<String> engines = EngineUtils.getEngineList();
        //通过 裸线程 进行并发查询,获取最先查到的答案
        String result = new FlavorThreadsDemo().getFirstResult("正则表达式", engines);
        //打印结果
        LOGGER.info(result);
    }
}

结果:


2018-03-06 22:32:46 INFO  FlavorThreadsDemo:29 - 通过裸线程进行并发编程,自己控制现场数量:6
2018-03-06 22:32:46 INFO  FlavorThreadsDemo:47 - 为搜索引擎[百度]创建1个线程...
2018-03-06 22:32:46 INFO  FlavorThreadsDemo:47 - 为搜索引擎[Google]创建2个线程...
2018-03-06 22:32:46 INFO  FlavorThreadsDemo:47 - 为搜索引擎[必应]创建3个线程...
2018-03-06 22:32:46 INFO  FlavorThreadsDemo:47 - 为搜索引擎[搜狗]创建4个线程...
2018-03-06 22:32:46 INFO  FlavorThreadsDemo:47 - 为搜索引擎[Redis]创建5个线程...
2018-03-06 22:32:46 INFO  EngineUtils:36 - 搜索引擎[Redis]正在查询,预计用时2484毫秒...
2018-03-06 22:32:46 INFO  EngineUtils:36 - 搜索引擎[Google]正在查询,预计用时419毫秒...
2018-03-06 22:32:46 INFO  FlavorThreadsDemo:47 - 为搜索引擎[Solr]创建6个线程...
2018-03-06 22:32:46 INFO  EngineUtils:36 - 搜索引擎[百度]正在查询,预计用时2093毫秒...
2018-03-06 22:32:46 INFO  EngineUtils:36 - 搜索引擎[搜狗]正在查询,预计用时4568毫秒...
2018-03-06 22:32:46 INFO  EngineUtils:36 - 搜索引擎[必应]正在查询,预计用时1022毫秒...
2018-03-06 22:32:46 INFO  EngineUtils:36 - 搜索引擎[Solr]正在查询,预计用时3937毫秒...
2018-03-06 22:32:47 INFO  FlavorThreadsDemo:67 - 通过搜索引擎[Google],首先查到关于(正则表达式)问题的结果,用时 = 419毫秒!

使用裸线程的主要优点是,你很接近并发计算的操作系统/硬件模型。并且这个模型非常简单:多个线程运行,通过共享内存通讯,就是这样

自己管理线程的最大劣势是,你很容易过分的关注线程的数量。线程是很昂贵的对象,创建它们需要耗费大量的内存和时间。这是一个矛盾,线程太少,你不能获得良好的并发性;线程太多,将很可能导致内存问题,调度也变得更复杂。

然而,如果你需要一个快速和简单的解决方案,你绝对可以使用这个方法,不要犹豫。

方法2:认真对待Executor和CompletionService

另一个选择是使用API来管理一组线程。幸运的是,JVM为我们提供了这样的功能,就是Executor接口。Executor接口的定义非常简单:

public interface Executor {
    void execute(Runnable command);
}

Executor接口隐藏了如何处理Runnable的细节。它仅仅说,“开发者!你只不过是一袋肉,给我任务,我会处理它!”

更酷的是,Executors类提供了一组方法,能够创建拥有完善配置的线程池和executor。我们将使用newFixedThreadPool(),它创建预定义数量的线程,并不允许线程数量超过这个预定义值。这意味着,如果所有的线程都被使用的话,提交的命令将会被放到一个队列中等待;当然这是由executor来管理的。

在它的上层,有ExecutorService管理executor的生命周期,以及CompletionService会抽象掉更多细节,作为已完成任务的队列。得益于此,我们不必担心只会得到第一个结果。

代码:

/**
 * 并发四种口味-02 Executor
 * Created by 韩超 on 2018/3/6.
 */
public class FlavorExecutorsDemo implements IFlavorDemo  {
    private final static Logger LOGGER = Logger.getLogger(FlavorExecutorsDemo.class);

    /**
     * <p>Title: 通过多个搜索引擎查询多个条件,并返回第一条查询结果</p>
     *
     * @param question 问题
     * @param engines  搜索引擎列表
     * @author 韩超 2018/3/6 10:07
     */
    @Override
    public String getFirstResult(String question, List<String> engines){
        //将查询结果放在"Atomic"变量中,保证原子性
        AtomicReference<String> result = new AtomicReference<String>();

        //通过Executors.newFixedThreadPool(size)创建固定大小的线程池,只能运行size数量的线程,其余线程等待
        //创建ExecutorService线程池,此线程池能够主动控制线程池的运行、关闭和终止
        ExecutorService service = Executors.newFixedThreadPool(3);
        LOGGER.info("通过Executors创建固定大小的线程池,线程池大小:3,当前线程数:" + Thread.activeCount() + "线程池最大线程数:" + (Thread.activeCount() + 3));
        try{
            //使用原子变量去测试 线程池提交服务 的是否有序
            AtomicInteger count = new AtomicInteger();

            //针对每一个搜索引擎,都调用一次service的submit()方法
            for (String engine : engines) {
                //lambda,通过service.submit()设置业务代码
                service.submit(
                        () -> {
                            LOGGER.info("为搜索引擎[" + engine + "]进行第" + count + "次服务提交...当前活跃线程数:" + Thread.activeCount());
                            count.getAndIncrement();
                            //调用某种搜索引擎进行搜索,并将搜索结果通过CAS方式放到result中
                            result.compareAndSet(null, EngineUtils.searchByEngine(question, engine));
                            return result;
                        }
                );
            }
            //当result取不到值时,证明还没有搜索引擎获取查出结果,通过while的无限循环进行等待
            while (null == result.get()) ;
        }finally {
            //记得要手动关闭ExecutorService线程池
            service.shutdown();
        }

        return result.get();
    }

    /**
     * <p>Title: 创建一组搜索引擎,对同一话题进行查询,并获取第一个查到的结果。</p>
     *
     * @author 韩超 2018/3/6 10:05
     */
    public static void main(String[] args) throws InterruptedException {
        //通过工具类获取搜索引擎列表
        List<String> engines = EngineUtils.getEngineList();
        //通过 executor 进行并发查询,获取最先查到的答案
        String result = new FlavorExecutorsDemo().getFirstResult("如何使用筷子?", engines);
        //打印结果
        LOGGER.info(result);
    }
}

结果:

2018-03-06 22:33:24 INFO  FlavorExecutorsDemo:33 - 通过Executors创建固定大小的线程池,线程池大小:3,当前线程数:2线程池最大线程数:5
2018-03-06 22:33:24 INFO  FlavorExecutorsDemo:43 - 为搜索引擎[Google]进行第0次服务提交...当前活跃线程数:5
2018-03-06 22:33:24 INFO  FlavorExecutorsDemo:43 - 为搜索引擎[百度]进行第0次服务提交...当前活跃线程数:5
2018-03-06 22:33:24 INFO  EngineUtils:36 - 搜索引擎[百度]正在查询,预计用时3251毫秒...
2018-03-06 22:33:24 INFO  FlavorExecutorsDemo:43 - 为搜索引擎[必应]进行第0次服务提交...当前活跃线程数:5
2018-03-06 22:33:24 INFO  EngineUtils:36 - 搜索引擎[Google]正在查询,预计用时1589毫秒...
2018-03-06 22:33:24 INFO  EngineUtils:36 - 搜索引擎[必应]正在查询,预计用时4199毫秒...
2018-03-06 22:33:25 INFO  FlavorExecutorsDemo:43 - 为搜索引擎[搜狗]进行第3次服务提交...当前活跃线程数:5
2018-03-06 22:33:25 INFO  FlavorExecutorsDemo:72 - 通过搜索引擎[Google],首先查到关于(如何使用筷子?)问题的结果,用时 = 1589毫秒!
2018-03-06 22:33:25 INFO  EngineUtils:36 - 搜索引擎[搜狗]正在查询,预计用时1340毫秒...
2018-03-06 22:33:27 INFO  FlavorExecutorsDemo:43 - 为搜索引擎[Redis]进行第4次服务提交...当前活跃线程数:5
2018-03-06 22:33:27 INFO  EngineUtils:36 - 搜索引擎[Redis]正在查询,预计用时4383毫秒...
2018-03-06 22:33:27 INFO  FlavorExecutorsDemo:43 - 为搜索引擎[Solr]进行第5次服务提交...当前活跃线程数:5
2018-03-06 22:33:27 INFO  EngineUtils:36 - 搜索引擎[Solr]正在查询,预计用时4173毫秒...

如果你需要精确的控制程序产生的线程数量,以及它们的精确行为,那么executor和executor服务将是正确的选择。例如,需要仔细考虑的一个重要问题是,当所有线程都在忙于做其他事情时,需要什么样的策略?增加线程数量或者不做数量限制?把任务放入到队列等待?如果队列也满了呢?无限制的增加队列大小?

感谢JDK,已经有很多配置项回答了这些问题,并且有着直观的名字,例如上面的Executors.newFixedThreadPool(4)。

线程和服务的生命周期也可以通过选项来配置,使资源可以在恰当的时间关闭。唯一的不便之处是,对新手来说,配置选项稍微有一些复杂和抽象。然而,在并发编程方面,你几乎找不到更简单的了。

总之,对于大型系统,我个人认为使用executor最合适

方法3:通过并行流,使用ForkJoinPool (FJP)

Java 8中加入了并行流,从此我们有了一个并行处理集合的简单方法。它和lambda一起,构成了并发计算的一个强大工具。

如果你打算运用这种方法,那么有几点需要注意。首先,你必须掌握一些函数编程的概念,它实际上更有优势。其次,你很难知道并行流实际上是否使用了超过一个线程,这要由流的具体实现来决定。如果你无法控制流的数据源,你就无法确定它做了什么。

另外,你需要记住,默认情况下是通过ForkJoinPool.commonPool()实现并行的。这个通用池由JVM来管理,并且被JVM进程内的所有线程共享。这简化了配置项,因此你不用担心。

代码:

/**
 * 并发四种口味-03 Fork/Join框架
 * Created by 韩超 on 2018/3/6.
 */
public class FlavorParallelDemo implements IFlavorDemo {
    private final static Logger LOGGER = Logger.getLogger(FlavorParallelDemo.class);

    /**
     * 通过多个搜索引擎查询多个条件,并返回第一条查询结果
     *
     * @param question 查询问题
     * @param engines  查询条件数组
     * @return 最先查出的结果
     * @author hanchao 2018/3/5 22:05
     */
    @Override
    public String getFirstResult(String question, List<String> engines) {
        LOGGER.info("使用默认并行流进行并发编程,默认划分的子任务数 = CPU内核数(4)");
        //使用原子变量去测试任务划分是否有序
        AtomicInteger count = new AtomicInteger();

        //用list.stream.parallel()开启并行流进行并发编程
        Optional<String> result = engines.stream().parallel().map(
                (engine) -> {
                    try {
                        LOGGER.info("CPU划分了第" + count + "个子任务....");
                        count.getAndIncrement();
                        return EngineUtils.searchByEngine(question, engine);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    return null;
                }
        ).findAny();//任何一个子任务完成都可以结束
        return result.get();
    }

    /**
     * <p>Title: 创建一组搜索引擎,对同一话题进行查询,并获取第一个查到的结果。</p>
     *
     * @author 韩超 2018/3/6 11:53
     */
    public static void main(String[] args) {
        //通过工具类获取搜索引擎列表
        List<String> engines = EngineUtils.getEngineList();
        //通过 并行操作 进行并发查询,获取最先查到的答案
        String result = new FlavorParallelDemo().getFirstResult("正则表达式", engines);
        //打印结果
        LOGGER.info(result);
    }
}

结果:

2018-03-06 22:33:44 INFO  FlavorParallelDemo:26 - 使用默认并行流进行并发编程,默认划分的子任务数 = CPU内核数(42018-03-06 22:33:44 INFO  FlavorParallelDemo:34 - CPU划分了第0个子任务....
2018-03-06 22:33:44 INFO  FlavorParallelDemo:34 - CPU划分了第0个子任务....
2018-03-06 22:33:44 INFO  FlavorParallelDemo:34 - CPU划分了第0个子任务....
2018-03-06 22:33:44 INFO  FlavorParallelDemo:34 - CPU划分了第0个子任务....
2018-03-06 22:33:44 INFO  EngineUtils:36 - 搜索引擎[百度]正在查询,预计用时2545毫秒...
2018-03-06 22:33:44 INFO  EngineUtils:36 - 搜索引擎[搜狗]正在查询,预计用时3158毫秒...
2018-03-06 22:33:44 INFO  EngineUtils:36 - 搜索引擎[Google]正在查询,预计用时1317毫秒...
2018-03-06 22:33:44 INFO  EngineUtils:36 - 搜索引擎[必应]正在查询,预计用时2503毫秒...
2018-03-06 22:33:47 INFO  FlavorParallelDemo:57 - 通过搜索引擎[Google],首先查到关于(正则表达式)问题的结果,用时 = 1317毫秒!

看上面的并行流(parallelStream)例子,我们不关心单独的任务在哪里完成,由谁完成。然而,这也意味着,你的应用程序中可能存在一些停滞的任务,而你却无法知道。在另一篇关于并行流的文章中,我详细地描述了这个问题。并且有一个变通的解决方案,虽然它并不是世界上最直观的方案。

ForkJoin是一个很好的框架,由比我更聪明的人来编写和预先配置。因此当我需要写一个包含并行处理的小型程序时,ForkJoin是我的第一选择。

ForkJoin最大的缺点是,你必须预见到它可能产生的并发症。如果对JVM没有整体上的深入了解,这很难做到。这只能来自于经验。

方法4:雇用一个Actor

Actor模型是对我们本文中所探讨的方法的一个奇怪的补充。JDK中没有actor的实现;因此你必须引用一些实现了actor的库。

简短地说,在actor模型中,你把一切都看做是一个actor。一个actor是一个计算实体,就像上面第一个例子中的线程,它可以从其他actor那里接收消息,因为一切都是actor。

在应答消息时,它可以给其他actor发送消息,或者创建新的actor并与之交互,或者只改变自己的内部状态。

相当简单,但这是一个非常强大的概念。生命周期和消息传递由你的框架来管理,你只需要指定计算单元是什么就可以了。另外,actor模型强调避免全局状态,这会带来很多便利。你可以应用监督策略,例如免费重试,更简单的分布式系统设计,错误容忍度等等。

下面是一个使用Akka Actors的例子。Akka Actors有Java接口,是最流行的JVM Actor库之一。实际上,它也有Scala接口,并且是Scala目前默认的actor库。Scala曾经在内部实现了actor。不少JVM语言都实现了actor,比如Fantom。这些说明了Actor模型已经被广泛接受,并被看做是对语言非常有价值的补充。

代码:

/**
 * Created by 韩超 on 2018/3/6.
 */
public class FlavorActorDemo implements IFlavorDemo {
    private final static Logger LOGGER = Logger.getLogger(FlavorActorDemo.class);

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

        public String getQuestion() {
            return question;
        }

        public void setQuestion(String question) {
            this.question = question;
        }

        public String getEngine() {
            return engine;
        }

        public void setEngine(String engine) {
            this.engine = engine;
        }

        public QueryTerms(String question, String engin) {
            this.question = question;
            this.engine = engin;
        }
    }

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

        public QueryResult(String result) {
            this.result = result;
        }

        public String getResult() {
            return result;
        }

        public void setResult(String result) {
            this.result = result;
        }
    }

    /**
     * <p>Title:搜索引擎Actor </br>
     * 继承UntypedAbstractActor成为一个Actor</p>
     *
     * @author 韩超 2018/3/6 14:42
     */
    static class SearchEngineAcotr extends UntypedAbstractActor {

        /**
         * <p>Title: Actor都需要重写消息接收处理方法</p>
         *
         * @author 韩超 2018/3/6 14:42
         */
        @Override
        public void onReceive(Object message) throws Throwable {
            //如果消息是指定的类型Message,则进行处理,否则不处理
            if (message instanceof QueryTerms) {
                //通过工具类进行一次搜索引擎查询
                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);
            }
        }
    }

    /**
     * <p>Title: 问题查询器Actor</br>
     * 继承自UntypedAbstractActor</p>
     *
     * @author 韩超 2018/3/6 16:31
     */
    static class QuestionQuerier extends UntypedAbstractActor {
        /**
         * 搜索引擎列表
         */
        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,则进行处理,否则不处理
                //通过CAS设置原子引用的值
                result.compareAndSet(null, ((QueryResult) message).getResult());
                //如果已经查询到了结果,则停止Actor
                //通过getContext()获取ActorSystem的上下文环境
                //通过getContext().stop(self())停止当前Actor
                getContext().stop(self());
            } else {//如果没有收到处理结果,则创建搜索引擎Actor进行查询

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

                //针对每一个搜索引擎,都创建一个Actor
                for (String engine : engines) {
                    LOGGER.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());
                }
            }
        }
    }

    /**
     * 通过多个搜索引擎查询多个条件,并返回第一条查询结果
     *
     * @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("searchByEngines");
        //创建一个原子引用用于保存查询结果
        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();
    }

    /**
     * <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);
        //打印结果
        LOGGER.info(result);

    }
}

结果:

2018-03-06 22:34:18 INFO  FlavorActorDemo:157 - 为百度创建第1个搜索引擎Actor....
2018-03-06 22:34:18 INFO  FlavorActorDemo:157 - 为Google创建第2个搜索引擎Actor....
2018-03-06 22:34:18 INFO  EngineUtils:36 - 搜索引擎[百度]正在查询,预计用时4894毫秒...
2018-03-06 22:34:18 INFO  FlavorActorDemo:157 - 为必应创建第3个搜索引擎Actor....
2018-03-06 22:34:18 INFO  EngineUtils:36 - 搜索引擎[Google]正在查询,预计用时3258毫秒...
2018-03-06 22:34:18 INFO  FlavorActorDemo:157 - 为搜狗创建第4个搜索引擎Actor....
2018-03-06 22:34:18 INFO  EngineUtils:36 - 搜索引擎[必应]正在查询,预计用时76毫秒...
2018-03-06 22:34:18 INFO  EngineUtils:36 - 搜索引擎[搜狗]正在查询,预计用时1869毫秒...
2018-03-06 22:34:18 INFO  FlavorActorDemo:157 - 为Redis创建第5个搜索引擎Actor....
2018-03-06 22:34:18 INFO  FlavorActorDemo:157 - 为Solr创建第6个搜索引擎Actor....
2018-03-06 22:34:18 INFO  EngineUtils:36 - 搜索引擎[Redis]正在查询,预计用时3403毫秒...
2018-03-06 22:34:18 INFO  EngineUtils:36 - 搜索引擎[Solr]正在查询,预计用时1165毫秒...
2018-03-06 22:34:18 INFO  FlavorActorDemo:212 - 通过搜索引擎[必应],首先查到关于(今天你吃了吗?)问题的结果,用时 = 76毫秒!
[INFO] [03/06/2018 22:34:23.797] [searchByEngines-akka.actor.default-dispatcher-7] [akka://searchByEngines/user/master] Message [pers.hanchao.flavors.FlavorActorDemo$QueryResult] from Actor[akka://searchByEngines/user/master/fetcher-2582786#-1241077001] to Actor[akka://searchByEngines/user/master#-316368035] 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'.

Akka actor在内部使用ForkJoin框架来处理工作。这里的代码很冗长,不要担心,大部分代码是消息类QueryItems(查询条件类)QueryResult(查询结果类)的定义,然后是两个不同的actor:QuestionQuerier(问题查询器)用来组织所有的搜索引擎,而SearchEngineActor(搜索引擎Actor)用来从给定的URL获取结果。这里代码行比较多是因为我不愿意把很多东西写在同一行上。Actor模型的强大之处来自于Props对象的接口,通过接口我们可以为actor定义特定的选择模式,定制的邮箱地址等。结果系统也是可配置的,只包含了很少的活动件。这是一个很好的迹象!

使用Actor模型的一个劣势是,它要求你避免全局状态,因此你必须小心的设计你的应用程序,而这可能会使项目迁移变得很复杂。同时,它也有不少优点,因此学习一些新的范例和使用新的库是完全值得的


总结

这篇文章中我们讨论了在Java应用中添加并行的几种不同方法。从我们自己管理Java线程开始,我们逐渐地发现更高级的解决方案,执行不同的executor服务ForkJoin框架actor计算模型

不知道当你面临真实问题时该如何选择?它们都有各自的优缺点,你需要在直观和易用性、配置和增加/减少机器性能等方面做出选择。


参考文献

[1] CompletionService/ExecutorCompletionService/线程池/concurrent包
[2] 深入浅出parallelStream
[3] Spring与Akka的集成
[4] akka(tell,ask,send)
[5] Akka2使用探索4(Actors)
[6] akka学习教程(四) actor生命周期
[7] Akka 通过Props实例创建Actor

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值