Java actor 并发 实例_Java并发的四种口味:Thread、Executor、ForkJoin、Actor

这篇文章讨论了Java(Scala)应用中的并行处理的多种方法。从简单的自己管理Java线程,到各种提供的已实现的解决方案:Executor(Java)、ForkJoin(Java 7)、Actor(Akka)。

在这篇文章中,将对同一个任务,用不同的代码实现并发的解决方案;然后,讨论这些方案有哪些优缺点,并且告诉你将会产生什么样的陷阱。

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

裸线程

Executors & Services

ForkJoin 并行框架 & 并行流

Actor 模型

任务

任务:实现一个方法-接受一条消息和一组字符串作为参数;字符串与搜索引擎的查询页面对应。对于每个字符串,这个方法都发出一个HTTP请求来查询消息,并返回第一条可用结构。

方法一:“原滋原味”裸线程

线程是并发的最基本单元。Java线程本质上是操作系统的线程,每个线程对象对应一个计算机底层线程。

每个线程有自己的栈空间。

线程的接口相当简明:只需要提供一个Runnable对象,调用.start()。没有已准备好的API来结束线程-需要自己实现;通过类似Boolean类型的标记来通讯。

private static String getFirstResult(String question, List engines) {

AtomicReference 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();

}

上面的例子中,对每个被查询的搜索引擎创建一个线程。查询结果返回到AtomicReference;不需要锁或者其他机制来保证只出现一次write操作。

优点:很接近并发计算的操作系统/硬件模型;结构简单。多个线程并行,通过共享内存通讯。

缺点:需要关注线程的数量。线程是很昂贵的对象-需要大量的内存和时间。这是一个矛盾-线程太少,不能获得良好的并发性;线程太多,开销太大,调度复杂。

方法二:“认真对待”Executor & ExecutorService

使用Java提供的API:Executor接口。

Executor接口定义非常简单:

public interface Executor {

void execute(Runnable command);

Executor隐藏了处理Runnable的细节;我们仅需要提供一个Runnable,它会处理。

Executors 类提供了一组方法,能够创建拥有完善配置的线程池和executor。

我们将使用newFixedThreadPool(),它创建预定义数量的线程,并不允许线程数量超过这个预定义值。这意味着,如果所有的线程都被使用的话,提交的命令将会被放到一个队列中等待;当然这是由executor来管理的。在它的上层,有ExecutorService管理executor的生命周期,以及CompletionService会抽象掉更多细节,作为已完成任务的队列。得益于此,我们不必担心只会得到第一个结果。

private static String getFirstResultExecutors(String question, List engines) {

ExecutorCompletionService service = new ExecutorCompletionService(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和ExecutorService将是正确的选择。例如:线程池满时:增加线程池数量?不做数量限制?把任务放入队列?队列满时:无限队列?

线程和服务的生命周期也可以通过选项来配置,使资源可以在恰当的时间关闭。

Executor API都非常直观的给出了解决方法-配置各种实现好的类。

对于大型、复杂系统,个人认为使用Executor最合适。

方法 三:并行流,ForkJoinPool(FJP)

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

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

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

private static String getFirstResult(String question, List engines) {

// get element as soon as it is available

Optional result = engines.stream().parallel().map((base) -> {

String url = base + question;

return WS.url(url).get();

}).findAny();

return result.get();

}

上面例子中,我们不关心单独的任务是在哪里完成的、由谁完成的。这也意味着,可能存在一些停滞的任务,我们却不知道。另一篇文章

ForkJoin是一个很好的并行框架-由一些聪明人编写和预先配置。

适用场景:当需要一个包含并行的小型程序时,是第一选择。(简单;已预先配置好。)

缺点:必须预先想到可能的并发问题;这很难,需要经验。

备注:Scala中,集合都用一个par() -等于Java 8 的parallel() 方法;返回一个并行计算的集合。Scala的建议是,对于可自由的结合的,是可被并行化的。如:reduceLeft()、foldLeft()不可;reduce()、fold()可并行化。

方法 四:雇佣 Actor

JDK中并没有Actor,这种方法其实是引入第三方类库,如:Akka-Scala中Actor的默认类库。

在Actor模型中,一切都是Actor。一个Actor是一个计算实体;它可以从其他Actor那里接受消息-接受消息;可以发送消息给其他Actor、或创建新Actor、或改变自己内部状态-应答消息。

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

Akka对于Java和Scala都有很好的API和文档,是最流行的JVM Actor库。

我们开始一个Akka Actor的例子。

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 engines;

private AtomicReference result;

public Querier(String question, List engines, AtomicReference 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 engines) {

ActorSystem system = ActorSystem.create("Search");

AtomicReference 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框架来处理工作。

缺点:Actor的设计哲学是避免全局状态;消息通讯。因此设计程序时要时刻小心,遵循Actor思想-这就很不灵活;项目迁移也很复杂,必须注意。

总结

四种Java并发编程方式:原始的Java线程、Executor、ForkJoin、Actor。

并发编程的几条心得:

不可变的一定是线程安全的;线程不安全的原因是状态可变。

并发编程不是指一个方法、一个类、一个框架;不能说使用java.concrrent包中的类写的代码就是线程安全的,不能说使用了上面四种方式编码就是线程安全的;并发编程是一种思想、一种处理并发的策略,不使用线程安全的类也可设计出线程安全的实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值