在之前的文章( 此处和此处 )中,我展示了当服务器负载沉重时,创建非阻塞异步应用程序可以提高性能。 EJB 3.1引入了@Asynchronous批注,用于指定方法将在将来的某个时间返回其结果。 Javadocs声明必须返回voidFuture 。 以下清单显示了使用此注释的服务示例:

Service2.java

@Stateless
public class Service2 {@Asynchronouspublic Future<String> foo(String s) {// simulate some long running processThread.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。

AsyncServlet2.java

@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 NOWCompletableFuture<String> cf = new CompletableFuture<>();service.foo(cf);// since we need to keep the response open, we need to start an async contextfinal 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并返回结果,该回调将被调用。 这里没有阻塞代码–没有线程被阻塞,没有线程被轮询,等待结果! 在负载下,服务器中的线程数可以保持最少,从而确保服务器可以高效运行,因为需要较少的上下文切换。

服务实现如下所示:

Service3.java

@Stateless
public class Service3 {@Asynchronouspublic void foo(CompletableFuture<String> cf) {// simulate some long running processThread.sleep(5000);cf.complete("bar");}
}

第7行确实很丑陋,因为它会阻塞,但假装这是代码调用大多数Web服务客户端和JDBC驱动程序会阻塞的API调用在Internet或慢速数据库中远程部署的Web服务。 或者,使用异步驱动程序 ,当结果可用时,完成第9行所示的将来。然后向CompletableFuture发出信号,可以调用在先前清单中注册的回调。

这不只是使用简单的回调吗? 这肯定是相似的,下面的两个清单显示了使用自定义回调接口的解决方案。

AsyncServlet3.java

@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
...

Service4.java

@Stateless
public class Service4 {@Asynchronouspublic void foo(Callback<String> c) {// simulate some long running processThread.sleep(5000);c.apply("bar");}public static interface Callback<T> {void apply(T t);}
}

同样,在客户端中,绝对没有任何阻塞。 但是,由于以下原因,使用CompletableFutureAsyncServlet2Service3类的早期示例更好些:

  • 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,该代码显示了一种异步服务方法,该服务方法试图做几件事以测试允许的操作。

Service5.java

@Stateless
public class Service5 {@Resource ManagedExecutorService mes;@Resource EJBContext ctx;@PersistenceContext(name="asdf") EntityManager em;@Asynchronouspublic 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 EMKeyValuePair kvp = new KeyValuePair("asdf");em.persist(kvp);Future<String> f = mes.submit(new Callable<String>() {@Overridepublic 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 EMKeyValuePair 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行可以找到该事务,它是否仍处于活动状态或容器是否已将其关闭也值得怀疑。

AsyncServlet5.java

@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

异步EJB只是一个Gi头吗?相关推荐

  1. ejb能调用另一个ejb吗_异步EJB只是一个Gi头吗?

    ejb能调用另一个ejb吗 在之前的文章( 此处和此处 )中,我展示了当服务器负载沉重时,创建非阻塞异步应用程序可以提高性能. EJB 3.1引入了@Asynchronous批注,用于指定方法将在将来 ...

  2. C语言实现了一个具有头结点的单链表(附完整源码)

    实现了一个具有头结点的单链表 有头结点的单链表 实现了一个具有头结点的单链表完整源码 有头结点的单链表 线性表的顺序存储结构的特点是逻辑关系上相邻的两个元素在物理位置上也相邻,因此可以随机存取表中的任 ...

  3. 爬虫-11-伪造电脑访问构建一个请求头

    什么都不做处理,直接请求的效果 经过一翻修饰后,再次请求 比较的结果 直接去拿数据,是拿不到的 需要适当的伪装一下自己 就好比... 七片服务器,带上请求头 直接访问,百度一看是 一个python 在 ...

  4. 怎么用C语言编写一个猪头的形状

    怎么用C语言编写一个猪头的形状 可以将问题修改为如何打印出黑白图案(C语言描述)?而不仅仅是猪头. 简单描述一下解决问题的机制. 首先是找到一张想要转换的图片.(废话) 第二步将其使用MATLAB等软 ...

  5. 万圣节快乐—用Python画一个南瓜头

    万圣节 万圣节又叫诸圣节,在每年的11月1日,是西方的传统节日,作为一个程序员怎么能不庆祝一下呢,接下来就利用 Python 中的 turtle 库画一个南瓜头出来: turtle库常用函数 函数 说 ...

  6. aac fhg lc哪一个模式_旅游没电别发愁,一个充电头,助你游遍全球

    我朋友汪伦.最近在计划出国旅游,我问他都准备了啥,他说根据网上的攻略,带了现金泡面老干妈还有一万个充电宝.现金我懂,国外还没普及扫码支付:泡面是怕外国菜吃不惯:老干妈是夹汉堡吃的:一万个充电宝是咋回事 ...

  7. 第一节:复习委托,并且通过委托的异步调用开启一个新线程和异步回调、异步等待。

    一. 再谈委托 1. 委托是一个关键字为delegate的自定义类型,通过委托可以把方法以参数的形式传递给另外一个方法,实现插件式的开发模式: 同时调用委托的时候,委托所包含的所有方法都会被实现. 2 ...

  8. 第一节:复习委托,并且通过委托的异步调用开启一个新线程和异步回调、异步等待

    一. 再谈委托 1. 委托是一个关键字为delegate的自定义类型,通过委托可以把方法以参数的形式传递给另外一个方法,实现插件式的开发模式: 同时调用委托的时候,委托所包含的所有方法都会被实现. 2 ...

  9. B 站 Up主自制秃头生成器,圆你一个秃头梦?

    本文经授权转载自公众号CSDN 公众号ID:CSDNnews 要说最近哪部剧最红,我说是<隐秘的角落>没人有意见吧? 看了这部片子,全国观众除了被男主张东升提醒爬山有风险之外,片中的另一个 ...

最新文章

  1. StringUtils.isEmpty和StringUtils.isBlank的区别
  2. 如何删除Android上ListViews之间的行?
  3. 年报系统课堂讨论记录
  4. python2d 平滑插值处理_python中平滑的、通用的2D线性插值
  5. 【C语言】 删除一个字符串中重复的字符
  6. 上传github代码
  7. plugin ‘org.springframework.boot:spring-boot-maven-plugin:‘not found
  8. 从零开始学习前端JAVASCRIPT — 14、闭包与继承
  9. 数字电子钟设计(基于quartus软件)
  10. Jumpserver docker部署及踩坑
  11. ArcGIS10.0专题图制作文档
  12. vivoX80Pro和华为P50Pro哪个值得入手参数对比
  13. DBeaver中的常用快捷键和自定义快捷键方式
  14. FPGA入门到实战-学习笔记
  15. 《欢乐颂2》狗血的剧情才是生活该有的模样
  16. 疫情之下欧洲汽车“众生相”
  17. [操作系统] 分页存储管理中的页表项、逻辑地址、物理地址的计算
  18. 教你如何用TreeView树形菜单创建像CSDN左边那样的导航之一:如何安装配置TreeView
  19. 有人感兴趣做兼职JAVA程序员吗? 地域不限,在家工作.
  20. 突然萌发关于 redis 的想法(2)

热门文章

  1. C++描述杭电OJ 2019. 数列有序 ||
  2. 银行营业网点管理系统——dao包(BranchesDao)
  3. java实现打印等腰三角形
  4. Servlet 登录时数据校验
  5. 2018蓝桥杯省赛---java---B---8(日志统计)
  6. 用limit 实现java的简单分页
  7. 旅游系统_数字洛江智慧旅游系统助力提升旅游安全水平
  8. 投票源码程序_[内附完整源码和文档] 基于JSP实现的影视创作论坛系统
  9. mybatis_user_guide(7) SQL语句构建器类
  10. java泛型程序设计——泛型类型的继承原则