多线程情况下使用scope=request级别的bean对象

最近在优化XX性能的时候,考虑根据产品维度进行并发处理,以便能够提升性能。

单个产品的业务处理是相互独立的,故可以很好利用并发的优势来提升执行速度。

但是由于之前在业务实现时,并没有过多考虑多线程的问题,故在改造时遇到了一些问题。

主要问题描述:

在子线程中调用scope=request级别的bean对象会报错,如下:

Scope 'request' is not active for the current thread

问题分析:

XX业务现在是根据日期(递增顺序)循环处理待处理的产品,而处理过程则是逐个产品进行处理。在复杂的业务处理中,为了减少数据库查询次数,对于一些service进行了数据缓存处理(比如净值数据)。数据缓存又不能太复杂,因为涉及到同步的问题,有可能某些数据随时会被修改,此时如果搞一套缓存同步的方案就太麻烦。

考虑到单次request请求过程中,这些缓存数据不应该被修改的实际情况,将service bean的scope设置为request,然后再将一些缓存数据放在该bean对象中,就可以很好地解决单次request层面的数据缓存实现。

正常情况下,这些bean对象能够在主线程中被访问到,但是当引入多线程时,就出现了上面的问题。

因为在创建的子线程里面,context里是没有requestScope的bean对象的。(具体原理参考下面的参考资料,主要的机制就是因为使用的是ThreadLocal来进行的bean对象存储)

解决方案:

搞清楚报错的原因后,针对性的解决方案也就差不多有思路了。

首先,需要解决子线程无法获取bean对象的问题。查看资料后,发现spring除了通过ThreadLocal来保存bean对象外,还实现了InheritableThreadLocal的方式。

那如何开启这个设置呢?

目前看到有两种方案:

1、直接通过web.xml里的SpringMVC的DispatcherServlet进行设置,参考设置如下:

<servlet>
    <servlet-name>springServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>threadContextInheritable</param-name>
      <param-value>true</param-value>
    </init-param>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath*:/spring-mvc*.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

2、通过重写RequestContextListener类来实现

具体参考下面的CSDN里的那篇文章,讲的很详细

(除了重写类,同样需要将web.xml里的listener类改为自己实现的类)

3、将request属性传给子线程(此方法也比较推荐)

先获取request的ServletRequestAttributes对象,然后传递给子线程,在子线程里调用set方法,把相关数据放入到当前线程的Thread Local里即可。

RequestAttributes attributes = RequestContextHolder.getRequestAttributes();

RequestContextHolder.setRequestAttributes(attributes);

说明:对于quartz定时任务而言,本身就根本没有ServletRequest对象,此时如果调用设置了scope的bean也会有类似问题。

通过方案3的变种,也能够很好的解决这个问题:

在任务线程起始处,直接执行

RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(new MockHttpServletRequest()));

此处使用了springframework的spring-test包,可以直接mock一个httpServletRequest对象。

该方案具体参考:(说明,文章最后的DummyRequest类,在Tomcat6版本中存在,但是在之后的版本并不存在了,故建议使用MockHttpServletRequest类)

Access spring Request scope cache in Singelton bean called from fork-join/thread pool - Java Code Geeks - 2023Interested to learn about Singelton bean? Check our article explaining how to enable cache in Spring who’s scope is to be accessed by a singleton bean.https://www.javacodegeeks.com/2020/05/access-spring-request-scope-cache-in-singelton-bean-called-from-fork-join-thread-pool.html

通过上面的修改后,子线程已经可以继承到主线程里的缓存对象,顺利获取到bean对象。

但是,又引入了一个新的问题:

问题描述:

为了方便使用多线程并发处理任务,直接使用了JAVA8的stream的parallel特性。具体代码如下:

Arrays.stream(funds).parallel().forEach(fundCode -> {// YOUR BUSINESS CODE });

通过stream可以很简单的实现并行处理任务,但是在实际运行过程中,第一次跑任务,顺利进行,没有报“Scope 'request' is not active for the current thread”的错误。但是之后再次跑任务,就又出现了这个错误。

问题分析:

研究了一下stream的parallel实现机制,大概了解到,其默认用到了forkjoin机制,通过forkjoin的线程池来做任务的并发处理。

然后又看了Stack Overflow上的那个帖子,基本上明白为啥会导致这个问题了。由于用到了线程池,那stream并发处理时所用的线程必然有可能是复用之前已经创建好的线程。在第一次跑任务的时候,新创建的线程会继承来自主线程的缓存对象(bean对象),此时能够正常运行。但是当第一次请求结束,由于缓存是request级别的,在结束的时候,自然会将bean对象销毁。此时线程池里的线程里保存的bean对象其实已经被销毁了。当第二次在跑任务的时候,复用到的线程里自然也就没有bean对象了,所以才会又报这个错误。

解决方案:

同样,搞清楚问题的原因后,方案也很有针对性了。既然线程复用导致了这个问题,那就想办法不复用线程,每次跑任务的时候,都重新创建一个新的线程即可。

此时也有两种方案:

1、自行实现并发任务的处理,自行创建和管理线程(或者自己实现一个线程池来保证单次request级别的线程复用)

但是这个实现就比较复杂了,失去了期初重构的初衷。

2、通过查看资料,发现stream默认使用的是forkJoin的common pool,那如果每次request的时候,都手动指定一个自定义的线程池,是不是就能保证线程不被跨request级别的复用了。于是研究了一下Stack Overflow里的帖子,发现给stream指定forkjoin线程池还是很简单的。参考代码如下:

final int parallelSize = 10;
ForkJoinPool forkJoinPool = null;
try {
  forkJoinPool = new ForkJoinPool(parallelSize);
  forkJoinPool.submit(() -> Arrays.stream(funds).parallel().forEach(code-> {
    // YOUR BUSINESS CODE
  })).get();
} catch (Exception e) {
  // catch the exception
} finally {
  if (forkJoinPool != null) {
    forkJoinPool.shutdown();
  }
}

指定自定义的线程池后,保证了每次request都会创建新的线程池,这些线程也都只会在单个request级别内进行reuse。至此,解决了本次重构所遇到的这些问题。

PS:需要注意的是,在使用forkjoin自定义线程池时,一定要在submit后面调用get方法,要不然主线程就不会等待子线程完成任务,就直接执行下面的代码,从而可能导致子线程还在跑任务,但是主线程已经返回了。(此时有可能后续的子线程执行时,又出现了找不到bean的错误)

PS2:需要注意forkjoinPool一定要在最后进行shutdown,释放资源,否则可能导致内存溢出的问题。

子线程使用父线程RequestScope作用域Bean问题的探究_weixin_34344677的博客-CSDN博客一、前言最近我们组在做项目分层模块化项目调研,同组通元童鞋在调研ajdk8的多租户方案需要对每一个请求开启一个线程进行处理,然后就产生一个问题如何在开启的线程中不破坏使用习惯情况下使用请求线程里面的RequestScope作用域的bean,感觉这个问题比较有意思就研究并整理下一下,以便备忘,下面从最基础知识将起,一步步引入问题和解决方法二、Threa..._子线程继承父线程sessionhttps://blog.csdn.net/weixin_34344677/article/details/89722447

spring - Scope 'session' is not active for the current thread; IllegalStateException: No thread-bound request found - Stack Overflowhttps://stackoverflow.com/questions/21286675/scope-session-is-not-active-for-the-current-thread-illegalstateexception-no

Spring MVC: How to use a request-scoped bean inside a spawned thread? - Stack Overflowhttps://stackoverflow.com/questions/14986519/spring-mvc-how-to-use-a-request-scoped-bean-inside-a-spawned-thread

Java 8: Parallel stream to wait until all the threads finishes the task - Stack Overflowhttps://stackoverflow.com/questions/51507144/java-8-parallel-stream-to-wait-until-all-the-threads-finishes-the-task

How to specify ForkJoinPool for Java 8 parallel stream? - Stack Overflowhttps://stackoverflow.com/questions/52287717/how-to-specify-forkjoinpool-for-java-8-parallel-stream

concurrency - Custom thread pool in Java 8 parallel stream - Stack Overflowhttps://stackoverflow.com/questions/21163108/custom-thread-pool-in-java-8-parallel-stream

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
在多线程下注入 Spring Bean,我们需要注意一些问题。首先,Spring Bean 的依赖注入是线程安全的,因为 Spring 容器会保证 Bean 的实例唯一,并且在多线程环境中不会出现竞态条件。然而,如果我们在多线程环境下手动创建 Bean 实例并注入,就需要注意线程安全性。 为了在多线程下注入 Spring Bean,我们需要确保 Bean 的作用域是线程安全的。通常情况下,我们可以将 Bean 的作用域设置为 prototype,使得每个线程都拥有自己的 Bean 实例,避免线程间的竞态条件。 另外,我们需要注意在多线程环境下对 Bean 的操作是否会造成线程安全问题。比如在单例 Bean使用了非线程安全的对象或方法,就可能会导致线程安全问题。在这种情况下,我们需要使用同步机制来保证线程安全,或者考虑将 Bean 的作用域设置为 prototype。 在注入 Bean 的时候,我们还需要考虑是否需要进行依赖注入或者手动创建 Bean 实例。如果需要在多线程下注入 Bean,最好使用 Spring 容器进行依赖注入,这样可以保证线程安全性并且简化代码逻辑。 总的来说,在多线程下注入 Spring Bean,我们需要确保 Bean 的作用域是线程安全的,并且在操作 Bean 的过程中注意线程安全性,避免出现竞态条件。同时尽量使用 Spring 容器进行依赖注入,避免手动创建 Bean 实例造成线程安全问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

金融码农

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值