一、使用Spring提供的并发线程池,只要在配置文件加入bean的配置即可。
因为应用是基于Spring的所以首先考虑使用Spring提供的并发线程池来做,但是这种方式程序运行期间很慢而且经常在开发环境将IDE搞得无响应,下面是org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor的配置:
<!-- Spring并发线程池 -->
<bean id="threadPool"
class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<!-- 核心线程数 -->
<property name="corePoolSize" value="3" />
<!-- 最大线程数 -->
<property name="maxPoolSize" value="100" />
<!-- 队列最大长度 >=mainExecutor.maxSize -->
<property name="queueCapacity" value="200" />
<!-- 线程池维护线程所允许的空闲时间 -->
<property name="keepAliveSeconds" value="200" />
<!-- 线程池对拒绝任务(无线程可用)的处理策略 ThreadPoolExecutor.CallerRunsPolicy策略 ,调用者的线程会执行该任务,如果执行器已关闭,则丢弃. -->
<property name="rejectedExecutionHandler">
<bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy" />
</property>
</bean>
二、Spring 自带的线程池参数说明
Spring 自带的线程池 配置参数
< bean id=“taskExecutor”
class=“org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor”>
< !-- 核心线程数 -->
< property name=“corePoolSize” value=“10” />
< !-- 最大线程数 -->
< property name=“maxPoolSize” value=“100” />
< !-- 队列最大长度 >=mainExecutor.maxSize -->
< property name=“queueCapacity” value=“1000” />
< !-- 线程池维护线程所允许的空闲时间 -->
< property name=“keepAliveSeconds” value=“300” />
< !-- 线程池对拒绝任务(无线程可用)的处理策略 -->
< property name=“rejectedExecutionHandler”>
< bean class=“java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy” />
</ property>
</ bean>
配置的属性字段说明如下:
corePoolSize:线程池维护线程的最少数量
keepAliveSeconds:允许的空闲时间
maxPoolSize:线程池维护线程的最大数量
queueCapacity:缓存队列
rejectedExecutionHandler:对拒绝task的处理策略
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务 如果任务被拒绝了,则由调用线程(提交任务的线程)直接执行此任务
线程池的参数运行原理:
如果此时线程池中的数量小于corePoolSize配置的值,即使线程池中的线程都处于空闲状态,也会创建新的线程来处理被添加的任务。
如果此时线程池中的数量等于 corePoolSize配置的值,但是缓冲队列 workQueue未满时,任务就会被放入缓冲队列中。
如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maxPoolSize,建新的线程来处理被添加的任务。
如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maxPoolSize,通过handler所指定的策略来处理此任务。
解释 处理任务的优先级为:
核心线程corePoolSize、
任务队列workQueue、
最大线程 maximumPoolSize,
如果以上三者都满了,
使用handler处理被拒绝的任务。
当线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,线程池可以动态的调整池中的线程数。
四种线程池拒绝策略
四种策略:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。 ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务 ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务。。。。。
spring自带线程池使用不当导致的死锁问题
Spring自带线程池使用很方便,不过在相对复杂的并发编程场景中,使用时还是需要根据使用场景仔细考虑配置,否则可能会遇到本文中提及的坑。 具体的代码参照 示例项目 https://github.com/qihaiyan/springcamp/tree/master/spring-taskexecutor-block
一、概述
spring自带线程池有2个核心配置,一个是线程池的大小,一个是队列的大小。 ThredPoolTaskExcutor的处理流程: 新建线程并处理请求,直到线程数大小等于corePoolSize; 将请求放入workQueue中,线程池中的空闲线程去workQueue中取任务并处理; 当workQueue满时,就新建线程并处理请求,当线程池子大小大小等于maximumPoolSize时,会用RejectedExecutionHandler来做拒绝处理。
Reject策略有四种:
(1)AbortPolicy策略,是默认的策略,拒绝请求并抛出异常RejectedExecutionException。
(2)CallerRunsPolicy策略 ,由调用线程执行任务.
(3)DiscardPolicy策略,拒绝请求但不抛出异常.
(4)DiscardOldestPolicy策略,丢弃最早进入队列的任务.
二、多个异步处理共用同一个线程池的异常情况
模拟一个耗时的操作,该操作通过Async注解设置为异步执行。Async会默认使用名为taskExecutor的线程池。该操作返回一个CompletableFuture,后续的处理中会等待该异步操作执行完成。
@Service
public class DelayService {
@Async
public CompletableFuture<String> delayFoo(String v) {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(v + " runs in thread: " + Thread.currentThread().getName());
return CompletableFuture.completedFuture(v);
}
}
设置线程池,将线程池大小设置为2,队列设置为一个比线程池大的值,此处为10。当队列大小大于等于线程池大小时,就会出现本文遇到的程序阻塞的问题。
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(10);
executor.setThreadNamePrefix("taskExecutor-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.initialize();
return executor;
}
}
并发处理:
while (true) {
try {
CompletableFuture.runAsync(
() -> CompletableFuture.allOf(Stream.of("1", "2", "3")
.map(v -> delayService.delayFoo(v))
.toArray(CompletableFuture[]::new)) // 将数组中的任务提交到线程池中
.join(), taskExecutor); // 通过join方法等待任务完成
} catch (Exception e) {
e.printStackTrace();
}
}
三、问题分析
程序启动后,很快就会阻塞,通过jstack查看线程状态,发现taskExecutor-1、taskExecutor-2、main三个线程都处在WAITING状态,等待CompletableFuture.join方法执行完成。
priority:5 - threadId:0x00007f7f8eb36800 - nativeId:0x3e03 - nativeId (decimal):15875 - state:WAITING
stackTrace:
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000007961fe548> (a java.util.concurrent.CompletableFuture$Signaller)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.CompletableFuture$Signaller.block(CompletableFuture.java:1693)
at java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3323)
at java.util.concurrent.CompletableFuture.waitingGet(CompletableFuture.java:1729)
at java.util.concurrent.CompletableFuture.join(CompletableFuture.java:1934)
通过分析程序的执行过程,不难发现阻塞的原因。 由于线程池设置的Queue的大小大于线程池的大小,当线程池满时,delayFoo方法会处在队列中,随着程序的执行,总会出现线程池中都是CompletableFuture.join方法,队列中都是delayFoo方法的情况。
这时候线程中的join方法在等待队列中的delayFoo方法执行完成,而队列中的delayFoo方法由于等不到可用线程而执行,整个程序就陷入了死锁状态。
解决的方法也很简单,就是将队列的大小设置为小于线程数的大小,这样队列中的方法就有机会拿到线程,从而不会因为线程占满而进入死锁状态。