在之前的文章( 此处和此处 )中,我展示了当服务器负载沉重时,创建非阻塞异步应用程序可以提高性能。 EJB 3.1引入了@Asynchronous
批注,用于指定方法将在将来的某个时间返回其结果。 Javadocs声明必须返回void
或Future
。 以下清单显示了使用此注释的服务示例:
Service2.java
@Stateless
public class Service2 {
@Asynchronous
public Future<String> foo(String s) {
// simulate some long running process
Thread.sleep(5000);
s += "<br>Service2: threadId=" + Thread.currentThread().getId();
return new AsyncResult<String>(s);
}
}
注释位于第4行。该方法返回String
类型的Future
,并在第10行通过将输出包装在AsyncResult
。 在客户端代码调用EJB方法时,容器将拦截该调用并创建一个任务,该任务将在另一个线程上运行,以便它可以立即返回Future
。 当容器然后使用另一个线程运行任务时,它将调用EJB的方法并使用AsyncResult
来完成给定调用者的Future
。 即使看起来与Internet上所有示例中的代码完全一样,此代码也存在一些问题。 例如, Future
类仅包含用于获取Future
结果的阻塞方法,而不包含用于在回调完成时注册回调的任何方法。 这将导致如下所示的代码,当容器处于加载状态时,这是很糟糕的:
//type 1
Future<String> f = service.foo(s);
String s = f.get(); //blocks the thread, but at least others can run
//... do something useful with the string...
//type 2
Future<String> f = service.foo(s);
while(!f.isDone()){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
...
}
}
String s = f.get();
//... do something useful with the string...
这种代码是不好的,因为它导致线程阻塞,这意味着它们在这段时间内无法做任何有用的事情。 当其他线程可以运行时,需要进行上下文切换,这会浪费时间和精力(有关成本或我以前的文章的结果,请参见这篇出色的文章)。 像这样的代码会使已经处于负载状态的服务器承受更大的负载,并停止运行。
那么是否有可能使容器异步执行方法,而编写不需要阻塞线程的客户端呢? 它是。 下面的清单显示了一个servlet。
@WebServlet(urlPatterns = { "/AsyncServlet2" }, asyncSupported = true)
public class AsyncServlet2 extends HttpServlet {
@EJB private Service3 service;
protected void doGet(HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
final PrintWriter pw = response.getWriter();
pw.write("<html><body>Started publishing with thread " + Thread.currentThread().getId() + "<br>");
response.flushBuffer(); // send back to the browser NOW
CompletableFuture<String> cf = new CompletableFuture<>();
service.foo(cf);
// since we need to keep the response open, we need to start an async context
final AsyncContext ctx = request.startAsync(request, response);
cf.whenCompleteAsync((s, t)->{
try {
if(t!=null) throw t;
pw.write("written in the future using thread " + Thread.currentThread().getId()
+ "... service response is:");
pw.write(s);
pw.write("</body></html>");
response.flushBuffer();
ctx.complete(); // all done, free resources
} catch (Throwable t2) {
...
第1行声明Servlet支持异步运行-不要忘记这一点! 第8-10行开始将数据写入响应,但有趣的位在第13行,其中调用了异步服务方法。 我们没有将Future
用作返回类型,而是向其传递了CompletableFuture
,它用于将结果返回给我们。 怎么样? 第16行代码将启动异步servlet上下文,因此我们仍然可以在doGet
方法返回后写入响应。 从第17行开始,然后有效地在CompletableFuture
上注册了一个回调,一旦完成CompletableFuture
并返回结果,该回调将被调用。 这里没有阻塞代码–没有线程被阻塞,没有线程被轮询,等待结果! 在负载下,服务器中的线程数可以保持最少,从而确保服务器可以高效运行,因为需要较少的上下文切换。
服务实现如下所示:
@Stateless
public class Service3 {
@Asynchronous
public void foo(CompletableFuture<String> cf) {
// simulate some long running process
Thread.sleep(5000);
cf.complete("bar");
}
}
第7行确实很丑陋,因为它会阻塞,但假装这是代码调用大多数Web服务客户端和JDBC驱动程序会阻塞的API调用在Internet或慢速数据库中远程部署的Web服务。 或者,使用异步驱动程序 ,当结果可用时,完成第9行所示的将来。然后向CompletableFuture
发出信号,可以调用在先前清单中注册的回调。
这不只是使用简单的回调吗? 这肯定是相似的,下面的两个清单显示了使用自定义回调接口的解决方案。
@WebServlet(urlPatterns = { "/AsyncServlet3" }, asyncSupported = true)
public class AsyncServlet3 extends HttpServlet {
@EJB private Service4 service;
protected void doGet(HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
...
final AsyncContext ctx = request.startAsync(request, response);
service.foo(s -> {
...
pw.write("</body></html>");
response.flushBuffer();
ctx.complete(); // all done, free resources
...
@Stateless
public class Service4 {
@Asynchronous
public void foo(Callback<String> c) {
// simulate some long running process
Thread.sleep(5000);
c.apply("bar");
}
public static interface Callback<T> {
void apply(T t);
}
}
同样,在客户端中,绝对没有任何阻塞。 但是,由于以下原因,使用CompletableFuture
的AsyncServlet2
和Service3
类的早期示例更好些:
-
CompletableFuture
的API允许出现异常/失败, -
CompletableFuture
类提供用于异步执行回调和相关任务的方法,即在fork-join池中,以便整个系统使用尽可能少的线程运行,从而可以更有效地处理并发性, - 可将
CompletableFuture
与其他对象结合使用,以便您可以注册仅在多个CompletableFuture
完成后才能调用的回调, - 回调不会立即被调用,而是池中有限数量的线程按它们应运行的顺序为
CompletableFuture
的执行提供服务。
在第一个清单之后,我提到异步EJB方法的实现存在一些问题。 除了阻塞客户端之外,另一个问题是,根据EJB 3.1 Spec的 4.5.3章,客户端事务上下文不会通过异步方法调用传播。 如果要使用@Asynchronous
批注创建两个可以并行运行并在单个事务中更新数据库的方法,则该方法将无效。 这在某种程度上限制了@Asynchronous
注释的使用。
使用CompletableFuture
,您可能认为可以在同一个事务上下文中并行运行多个任务,方法是先在EJB中启动一个事务,然后创建多个可运行对象,然后使用runAsync
方法运行它们,该方法在执行中运行它们池,然后注册一个回调以使用allOf
方法完成所有操作后allOf
。 但是您可能会因为多种原因而失败:
- 如果您使用容器管理的事务,那么一旦导致事务开始的EJB方法将控制权返回给容器,事务将被提交-如果那时您的期货还没有完成,则您将不得不阻塞运行EJB方法的线程这样它就等待并行执行的结果,而阻塞正是我们要避免的,
- 如果运行任务的单个执行池中的所有线程都被阻塞,等待它们的数据库调用应答,那么您将有可能创建性能不佳的解决方案–在这种情况下,您可以尝试使用非阻塞的异步驱动程序 ,但不能每个数据库都有这样的驱动程序,
- 一旦任务在不同的线程(例如执行池中的线程)上运行,线程本地存储(TLS)就不再可用,因为正在运行的线程与将工作提交到执行池并进行设置的线程不同在提交工作之前将值存入TLS,
- 诸如
EntityManager
类的资源不是线程安全的 。 这意味着你无法通过EntityManager
成提交给池的任务,而每个任务需要得到它自己的保持EntityManager
实例,而是创建EntityManager
取决于TLS(见下文)。
让我们通过以下代码更详细地考虑TLS,该代码显示了一种异步服务方法,该服务方法试图做几件事以测试允许的操作。
@Stateless
public class Service5 {
@Resource ManagedExecutorService mes;
@Resource EJBContext ctx;
@PersistenceContext(name="asdf") EntityManager em;
@Asynchronous
public void foo(CompletableFuture<String> cf, final PrintWriter pw) {
//pw.write("<br>inside the service we can rollback, i.e. we have access to the transaction");
//ctx.setRollbackOnly();
//in EJB we can use EM
KeyValuePair kvp = new KeyValuePair("asdf");
em.persist(kvp);
Future<String> f = mes.submit(new Callable<String>() {
@Override
public String call() throws Exception {
try{
ctx.setRollbackOnly();
pw.write("<br/>inside executor service, we can rollback the transaction");
}catch(Exception e){
pw.write("<br/>inside executor service, we CANNOT rollback the transaction: " + e.getMessage());
}
try{
//in task inside executor service we CANNOT use EM
KeyValuePair kvp = new KeyValuePair("asdf");
em.persist(kvp);
pw.write("...inside executor service, we can use the EM");
}catch(TransactionRequiredException e){
pw.write("...inside executor service, we CANNOT use the EM: " + e.getMessage());
}
...
第12行没有问题,您可以回滚当容器调用EJB方法时在第9行自动启动的事务。 但是该事务将不是可能由调用第9行的代码启动的全局事务。第16行也没问题,您可以使用EntityManager
写入由第9行开始的事务内部的数据库。显示了在不同线程上运行代码的另一种方式,即使用Java EE 7中引入的ManagedExecutorService
。但是,这在任何时候都依赖TLS时也都会失败,例如,第22行和第31行会导致异常,因为在第9行启动的事务无法定位,因为使用TLS进行定位,并且第21-35行的代码使用与第19行之前的代码不同的线程运行。
下一个清单显示,第11-14行在CompletableFuture
上注册的完成回调也与第4-10行在不同的线程中运行,因为在第6行的回调之外启动提交事务的调用将在第6行失败再次参见图13,因为第13行的调用在TLS中搜索当前事务,并且因为运行第13行的线程与运行第6行的线程不同,所以找不到事务。 实际上,下面的清单实际上有一个不同的问题:处理对Web服务器的GET
请求的线程运行第JBAS010152: APPLICATION ERROR: transaction still active in request with status 0
和11行,然后返回,此时JBoss日志JBAS010152: APPLICATION ERROR: transaction still active in request with status 0
–即使线程运行第13行可以找到该事务,它是否仍处于活动状态或容器是否已将其关闭也值得怀疑。
@Resource UserTransaction ut;
@Override
protected void doGet(HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
ut.begin();
...
CompletableFuture<String> cf = new CompletableFuture<>();
service.foo(cf, pw);
...
cf.whenCompleteAsync((s, t)->{
...
ut.commit(); // => exception: "BaseTransaction.commit - ARJUNA016074: no transaction!"
});
}
事务显然依赖于线程和TLS。 但这不仅仅是依赖TLS的事务。 以JPA为例,该JPA被配置为直接在TLS中存储会话(即与数据库的连接) ,或者被配置为将该会话的范围限定为当前的JTA事务 ,而该事务又依赖于TLS。 或者使用从EJBContextImpl.getCallerPrincipal
提取的Principal
进行安全检查,该Principal
调用AllowedMethodsInformation.checkAllowed ,然后调用使用TLS的CurrentInvocationContext
并简单地返回(如果在TLS中找不到上下文),而不是进行适当的权限检查如第112行所示。
这些对TLS的依赖意味着,在使用CompletableFuture
或Java SE fork-join池或其他线程池(无论是否由容器管理)时,许多标准Java EE功能将不再起作用。
为了对Java EE公平起见,我在这里所做的事情都按设计工作! 规范实际上禁止在EJB容器中启动新线程。 我记得十多年前我曾经使用过旧版本的Websphere进行过一次测试-启动一个线程会引发异常,因为该容器确实严格遵守规范。 这是有道理的:不仅因为线程数应由容器管理,还因为Java EE对TLS的依赖意味着使用新线程会导致问题。 从某种意义上讲,这意味着使用CompletableFuture
是非法的,因为它使用了不受容器管理的线程池(该池由JVM管理)。 使用Java SE的ExecutorService
也是如此。 Java EE 7的ManagedExecutorService
是一个特例-它是规范的一部分,因此您可以使用它,但是您必须了解这样做的含义。 EJB上的@Asynchronous
批注也是如此。
结果是可以在Java EE容器中编写异步非阻塞应用程序,但是您确实必须知道自己在做什么,并且可能必须手动处理安全性和事务之类的事情,这确实是个问题。首先使用Java EE容器的原因。
那么是否有可能编写一个容器来消除对TLS的依赖以克服这些限制? 的确如此,但是解决方案并不仅仅依赖于Java EE。 该解决方案可能需要更改Java语言。 许多年前,在依赖注入之前,我曾经写过POJO服务,它在方法之间传递了JDBC连接,即作为服务方法的参数。 我这样做是为了可以在同一事务内(即在同一连接上)创建新的JDBC语句。 我所做的与JPA或EJB容器所需要做的事情并没有什么不同。 但是,现代框架没有使用TLS作为显式传递连接或用户之类的东西的方式,而是将TLS作为集中存储“上下文”的位置,即连接,事务,安全信息等。 只要您在同一线程上运行,TLS就是隐藏此类样板代码的好方法。 让我们假装TLS从未被发明过。 我们如何在不强制每种方法都将其作为参数的情况下传递上下文? Scala的implicit
关键字是一种解决方案。 您可以声明参数可以隐式定位,这使编译器难以将其添加到方法调用中。 因此,如果Java SE引入了这样的机制,则Java EE不需要依赖TLS,我们可以构建真正的异步应用程序,在该应用程序中,容器可以像今天一样通过检查注释来自动处理事务和安全性! 也就是说,当使用同步Java EE时,容器会知道何时提交事务-在启动事务的方法调用结束时。 如果您异步运行,则需要显式关闭事务,因为容器不再知道何时执行此操作。
当然,保持不阻塞的需要以及因此不依赖TLS的需要在很大程度上取决于当前的方案。 我不认为我今天在这里描述的问题是当今的普遍问题,而是解决市场利基市场的应用程序所面临的问题。 只需看一下Java EE优秀工程师目前正在提供的工作数量,而同步编程就是其中的标准。 但是我确实相信,规模更大的IT软件系统将变得越来越多,它们处理的数据越多,阻塞API就会成为一个问题。 我还认为,当前硬件增长速度的放缓使这个问题更加复杂。 有趣的是,Java是否a)是否需要跟上异步处理的趋势,以及b)Java平台是否会采取行动来固定对TLS的依赖。
翻译自: https://www.javacodegeeks.com/2015/08/is-asynchronous-ejb-just-a-gimmick.html