并发编程一直很棘手。从多个线程访问共享状态是一个中心问题,很容易出现难以捕获的错误。Java拥有安全和完美并发的所有工具,但是编译器会故意让开发人员编写危险的代码。现在迫切需要更高层次的框架来确保安全的并发编程。
Actor模型
actor模型是实现安全并发的方法之一。actors是可能具有可变状态并通常遵循标准规则的对象(Java意义上的类实例):
- 一切都是actor。actors之间通过发送异步消息进行独占通信。没有共享状态、公共静态变量等。只有actors可以更改其状态。
- 发送给actors的消息是按顺序处理的:尽管消息处理程序可能在不同的线程中被调用,但框架保证actors的状态更改是安全的,并且对所有后续消息处理程序都可见。
现有的actor框架有什么问题
然而,当我研究流行的Java actor框架时,我有一种奇怪的感觉。我试过akka,actor4j,kilim,quasar,reactors.io,orbit,offbynull/actors,edescortis/actor,vlingo actors,pcd actors,所有这些在我看来都是一样的……错了。
让我们来看看akka的一个简单的actor示例,akka是JVM最流行的actor框架之一:
public class MyActor extends AbstractActor {
public Receive createReceive() {
return receiveBuilder().match(SomeMessage.class, msg -> {
doSomeCommandProcessing(msg);
...
}).match(AnotherMessage.class, anotherMessage -> {
doAnotherMessageProcessing(anotherMessage);
...
}).build();
}
}
2019年,在这个Hello World的例子中,什么让我感到奇怪?
此API不是类型安全的。actor实现中的处理程序代码检查传入的消息类型(其中有一种instanceof)并正确地分派消息。但是我可以向任何actors发送任何消息,并且没有编译时检查来防止这种情况。
您需要从抽象类扩展。我讨厌从框架类扩展应用程序类。它产生了业务逻辑和库的丑陋组合,使我回到了使用ejbbean的旧时代。而且,如果我想的话,这不允许我从基类扩展。
必须为每种类型的消息创建一个类。在大多数情况下,这将是一个经典的不可变DTO,只有字段+构造函数+getter,所有这些烦人的样板行。不过没什么大不了的,JDK14记录类应该会更好,但正如我稍后将展示的那样,这是不必要的。
有趣的是,对于我所研究的大多数actors框架,API都有相同的问题(但是请参见下面的一些例外)。
更好的消息调用
“A message is a Java object”仍然是传统actor框架的基石。让我们拒绝这个概念。Java有更好的消息发送方式:调用类方法。(注意,在Smalltalk中,向对象发送消息并调用对象的方法是等价的概念)。明显的好处是:
- 该协议是静态定义的:方法签名定义actor类支持哪些方法(消息)以及接受哪些类型的消息(参数);
- 不需要为消息类型创建DTO类,多个消息参数自然由多个方法参数实现。
使用这种方法,我们可能会针对问题1和3-太好了!
我可以使用这种方法找到2个Java actor框架:
Akka类型的actors使用JDK代理包装对象并返回一个安全接口,该接口将普通Java方法调用转换为异步调用。有了接口平方器及其实现SquarerImpl,就可以得到一个“安全”句柄:
Squarer safeSquarer = TypedActor.get(system)
.typedActorOf(new TypedProps<SquarerImpl>(Squarer.class, SquarerImpl.class));
现在,对safesquare上void返回方法的调用将导致tell async actor调用,对值返回方法的调用将阻塞,直到该方法在其线程中返回为止(同步调用)。当一个方法返回一个未来时,真正的异步ask方法调用是可能的:
interface Squarer {
void setParam(int param); // async tell
double square(double val); // sync ask (blocking call)
Future<Double> squareF(double val); // async ask
}
Jumi Actors也有类似的方法,但它只支持tell消息:
ActorRef<Squarer> safeSquarer = actorThread.bindActor(Squarer.class,
SquarerImpl.class);
safeSquarer.tell().setParam(param);
但是,这两个lib都不适合异步ask。打字actor对回归未来的要求并不优雅,Jumi根本不支持提问。
构建一个更好的actors框架
通过将方法调用包装在高阶函数中而不是代理接口,我们可以非常优雅地实现tell和ask:
IActorRef<Squarer> safeSquarer = system.actorOf(Squarer.class);
safeSquarer.tell(squarer -> squarer.setParam(param)); //async tell
safeSquarer.ask(squarer -> squarer.square(5),
result -> System.out.println(result)); //async ask v1
CompletableFuture<Double> safeSquarer.ask(squarer ->
squarer.square(5)); //async ask v2
此API的优点:
- async-tell和ask的优雅而安全的实现。上述三个问题都解决了。
- 不需要有单独的接口和actor的实现。上例中的平方可以是接口或具体类。
- 现在对actor类完全没有限制。无需扩展框架抽象类,无需返回特殊类型。任何类现在都可以是actor类,甚至是第三方类(将非线程安全库包装成一个单线程actor并从多个线程中使用它现在非常容易!)
我在一个名为actr的轻量级库中实现了这种方法:https://github.com/zakgof/actr。
它没有按路径寻址actors或分布式actors支持等高级功能,但由于它是轻量级的,因此在一些简单的基准上与akka相比,它表现出了良好的性能:https://github.com/zakgof/akka-actr-benchmark。
actr包含了一些简单的调度程序(以及在需要时提供自己的调度程序的能力)。它还支持JDK14 EA中的虚拟线程-欢迎您尝试。
原文:https://medium.com/@zakgof/type-safe-actor-model-for-java-7133857a9f72
本文:http://jiagoushi.pro/node/1021
讨论:请加入知识星球或者微信圈子【首席架构师圈】
- 登录 发表评论
- 143 次浏览
最新内容
- 5 days ago
- 6 days 22 hours ago
- 1 week ago
- 1 week 1 day ago
- 1 week 1 day ago
- 1 week 1 day ago
- 1 week 2 days ago
- 1 week 2 days ago
- 1 week 2 days ago
- 1 week 2 days ago