`
2277259257
  • 浏览: 495802 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

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

 
阅读更多

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

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

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

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

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

• 裸线程

• Executors和Services

• ForkJoin框架和并行流

• Actor模型

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

对了,最后一件事:在文章最后,有一个小调查,关于你或者你的组织正在使用哪种并发模式。为了你的工程师同胞们,请填一下调查!

任务

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

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

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

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

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

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
private static String getFirstResult(String question, List<String> engines) {
 AtomicReference<String> result = new AtomicReference<>();
 for(String base: engines) {
   String url = base + question;
   new Thread(() -> {
     result.compareAndSet(null, WS.url(url).get());
   }).start();
 }
 while(result.get() == null); // wait for some result to appear
 return result.get();
}

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

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

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

方法2:认真对待Executor和CompletionService

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

1
2
3
4
5
public interface Executor {
 
void execute(Runnable command);
 
}

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

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

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

下面service.take()的一次调用将会只返回一个结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static String getFirstResultExecutors(String question, List<String> engines) {
 ExecutorCompletionService<String> service = new ExecutorCompletionService<String>(Executors.newFixedThreadPool(4));
 
 for(String base: engines) {
   String url = base + question;
   service.submit(() -> {
     return WS.url(url).get();
   });
 }
   try {
     return service.take().get();
   }
   catch(InterruptedException | ExecutionException e) {
     return null;
   }
}

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

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

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

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

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

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

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

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

1
2
3
4
5
6
7
8
private static String getFirstResult(String question, List<String> engines) {
 // get element as soon as it is available
 Optional<String> result = engines.stream().parallel().map((base) -> {
   String url = base + question;
   return WS.url(url).get();
 }).findAny();
 return result.get();
}

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

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模型已经被广泛接受,并被看做是对语言非常有价值的补充。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
static class Message {
 String url;
 Message(String url) {this.url = url;}
}
static class Result {
 String html;
 Result(String html) {this.html = html;}
}
 
static class UrlFetcher extends UntypedActor {
 
 @Override
 public void onReceive(Object message) throws Exception {
   if (message instanceof Message) {
     Message work = (Message) message;
     String result = WS.url(work.url).get();
     getSender().tell(new Result(result), getSelf());
   } else {
     unhandled(message);
   }
 }
}
 
static class Querier extends UntypedActor {
 private String question;
 private List<String> engines;
 private AtomicReference<String> result;
 
 public Querier(String question, List<String> engines, AtomicReference<String> result) {
 
   this.question = question;
   this.engines = engines;
   this.result = result;
 }
 
 @Override public void onReceive(Object message) throws Exception {
   if(message instanceof Result) {
     result.compareAndSet(null, ((Result) message).html);
     getContext().stop(self());
   }
   else {
     for(String base: engines) {
       String url = base + question;
       ActorRef fetcher = this.getContext().actorOf(Props.create(UrlFetcher.class), "fetcher-"+base.hashCode());
       Message m = new Message(url);
       fetcher.tell(m, self());
     }
   }
 }
}
 
private static String getFirstResultActors(String question, List<String> engines) {
 ActorSystem system = ActorSystem.create("Search");
 AtomicReference<String> result = new AtomicReference<>();
 
 final ActorRef q = system.actorOf(
   Props.create((UntypedActorFactory) () -> new Querier(question, engines, result)), "master");
 q.tell(new Object(), ActorRef.noSender());
 
 while(result.get() == null);
 return result.get();
}

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

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

分享到:
评论

相关推荐

    2024职工群体户外交友拓展“躺进春天 趣野人生”活动策划方案ss.pptx

    2024职工群体户外交友拓展“躺进春天 趣野人生”活动策划方案ss.pptx

    pypy3.7-v7.3.4-osx64.tar.bz2

    Python库是一组预先编写的代码模块,旨在帮助开发者实现特定的编程任务,无需从零开始编写代码。这些库可以包括各种功能,如数学运算、文件操作、数据分析和网络编程等。Python社区提供了大量的第三方库,如NumPy、Pandas和Requests,极大地丰富了Python的应用领域,从数据科学到Web开发。Python库的丰富性是Python成为最受欢迎的编程语言之一的关键原因之一。这些库不仅为初学者提供了快速入门的途径,而且为经验丰富的开发者提供了强大的工具,以高效率、高质量地完成复杂任务。例如,Matplotlib和Seaborn库在数据可视化领域内非常受欢迎,它们提供了广泛的工具和技术,可以创建高度定制化的图表和图形,帮助数据科学家和分析师在数据探索和结果展示中更有效地传达信息。

    腾讯&阿里&携程面试题汇总(精华版).pdf

    腾讯&阿里&携程面试题汇总(精华版)

    pypy2-v6.0.0-s390x.tar.bz2

    Python库是一组预先编写的代码模块,旨在帮助开发者实现特定的编程任务,无需从零开始编写代码。这些库可以包括各种功能,如数学运算、文件操作、数据分析和网络编程等。Python社区提供了大量的第三方库,如NumPy、Pandas和Requests,极大地丰富了Python的应用领域,从数据科学到Web开发。Python库的丰富性是Python成为最受欢迎的编程语言之一的关键原因之一。这些库不仅为初学者提供了快速入门的途径,而且为经验丰富的开发者提供了强大的工具,以高效率、高质量地完成复杂任务。例如,Matplotlib和Seaborn库在数据可视化领域内非常受欢迎,它们提供了广泛的工具和技术,可以创建高度定制化的图表和图形,帮助数据科学家和分析师在数据探索和结果展示中更有效地传达信息。

    基于C语言实现列车车厢重排问题(源码)

    列车车厢重排问题是经典的组合优化问题,也称为车厢调度问题或车厢排序问题。它的问题描述如下:有一列火车,列车由多节车厢组成,每个车厢上都有一个唯一的标识号。现在需要将这些车厢按照指定的顺序重新排列,使得满足一定的条件,例如车厢编号的升序或降序排列,或者满足某些车厢之间的关系等。

    报告-《新范式 新时代 新机会》dr.pptx

    报告-《新范式 新时代 新机会》dr.pptx

    xx集团数字化转型方案ss.pptx

    xx集团数字化转型方案ss.pptx

    pypy3.7-v7.3.3rc2-osx64.tar.bz2

    Python库是一组预先编写的代码模块,旨在帮助开发者实现特定的编程任务,无需从零开始编写代码。这些库可以包括各种功能,如数学运算、文件操作、数据分析和网络编程等。Python社区提供了大量的第三方库,如NumPy、Pandas和Requests,极大地丰富了Python的应用领域,从数据科学到Web开发。Python库的丰富性是Python成为最受欢迎的编程语言之一的关键原因之一。这些库不仅为初学者提供了快速入门的途径,而且为经验丰富的开发者提供了强大的工具,以高效率、高质量地完成复杂任务。例如,Matplotlib和Seaborn库在数据可视化领域内非常受欢迎,它们提供了广泛的工具和技术,可以创建高度定制化的图表和图形,帮助数据科学家和分析师在数据探索和结果展示中更有效地传达信息。

    Qlik Sense Desktop 2024

    Qlik Sense Desktop 是一个 Windows 应用程序,让个人用户有机会使用 Qlik Sense 并通过多个数据源以简便的拖放操作创建个性化、交互式数据可视化、报告和仪表板。个人和内部业务使用时免费。 您可以创建自己可以重复使用的 Qlik Sense 应用程序,并且可以修改和与他人共享,而不需要部署和管理大量的商业应用程序。 Qlik Sense是一个强大的可视化BI报表开发平台,Qlik Sense Desktop是Windows桌面版的客户端,通常还有Enterprise HUB云端版。

    Java项目源码_ssm_高校学生选课系统项目源码.rar

    **Java项目源码_ssm_高校学生选课系统项目源码.rar** 本资源为一个基于Java技术栈的高校学生选课系统项目源码,采用了SSM(Spring、SpringMVC、MyBatis)框架进行开发。项目涵盖了学生选课、课程管理、教师管理、学生管理等功能模块,可以为高校教务管理提供有力支持。 项目结构清晰,模块化设计,便于二次开发和定制。主要包括以下几个模块: 1. 用户认证模块:包括用户登录、注册、权限验证等功能,支持多角色(学生、教师、管理员)登录和权限控制。 2. 学生选课模块:学生可以查看课程信息、选课、退课等操作,支持选课时间、人数限制等规则。 3. 课程管理模块:教师可以发布课程、编辑课程信息、上传课程资源等操作,支持课程分类、标签等功能。 4. 教师管理模块:管理员可以添加、编辑、删除教师信息,支持教师角色权限分配。 5. 学生管理模块:管理员可以添加、编辑、删除学生信息,支持学生角色权限分配。 6. 成绩管理模块:教师可以录入、编辑、查看学生成绩,支持成绩统计和导出功能。 7. 通知公告模块:管理员可以发布通知公告,支持公告分类、置顶等功能。 8. 系统设置模块:管理员可以配置系统参数、数据备份、日志查看等操作,保障系统稳定运行。 本项目源码可以为高校教务管理提供有力支持,同时也适用于企业内部培训、教育培训等场景。如有需要,可以根据实际需求进行二次开发和定制,以满足不同场景的需求。

    asp代码ASP.NET+SQLBS模式的计算机等级考试管理系统的设计与实现(论文+源代码+开题报告)

    asp代码ASP.NET+SQLBS模式的计算机等级考试管理系统的设计与实现(论文+源代码+开题报告)本资源系百度网盘分享地址

    pypy2.7-v7.3.6-s390x.tar.bz2

    Python库是一组预先编写的代码模块,旨在帮助开发者实现特定的编程任务,无需从零开始编写代码。这些库可以包括各种功能,如数学运算、文件操作、数据分析和网络编程等。Python社区提供了大量的第三方库,如NumPy、Pandas和Requests,极大地丰富了Python的应用领域,从数据科学到Web开发。Python库的丰富性是Python成为最受欢迎的编程语言之一的关键原因之一。这些库不仅为初学者提供了快速入门的途径,而且为经验丰富的开发者提供了强大的工具,以高效率、高质量地完成复杂任务。例如,Matplotlib和Seaborn库在数据可视化领域内非常受欢迎,它们提供了广泛的工具和技术,可以创建高度定制化的图表和图形,帮助数据科学家和分析师在数据探索和结果展示中更有效地传达信息。

    星闪技术介绍.pptx

    星闪技术介绍

    《情绪管理指南》培训课件ld.pptx

    《情绪管理指南》培训课件ld.pptx

    毕业设计,前端,前端毕业设计

    毕业设计,前端,前端毕业设计

    课件-咨询IT规划方法dr.pptx

    课件-咨询IT规划方法dr.pptx

    非暴力沟通生命的语言lg.pptx

    非暴力沟通生命的语言lg.pptx

    nacos2.3.1改造适配postgresql配置文件

    nacos2.3.1改造适配postgresql配置文件,无需自己再去做数据库适配,导入数据库即可;

    高级网络人才培训专家-X00070003 第30章 配置PPP

    高级网络人才培训专家_X00070003 第30章 配置PPP

    C#的基础知识文档.doc

    C#的基础知识文档.doc

Global site tag (gtag.js) - Google Analytics