本文记录实际项目中使用线程时遇到的相关问题,阐述、分析了产生的问题,最终给出解决方案。
问题描述
Tomcat状态监控页面,点击停止按钮,页面一直显示正在停止,后台进程也一直显示正在停止,通过jstack分析,Tomcat关不掉是因为组件线程没有正常关闭导致1
2
3
4
5
6
7
8
9
10
11
12
13"pool-5-thread-1" #89 prio=5 os_pion=0 tid=0x000000003561b800 nid=0x1743c waiting on condition [0x000000004413e000]
java.lang.Thread.State: TIMED_WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x000000073a308cb0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at com.*.*.*.notify.consumer.datadictionary.ListenerEventQueue.takeDataDictionary(ListenerEventQueue.java:2039)
at com.*.*.*.notify.consumer.datadictionary.DatadictionaryEventConsumer.updateDatadictionary(DatadictionaryEventConsumer.java:62)
at com.*.*.*.notify.consumer.datadictionary.AbstractEventConsumer.run(AbstractEventConsumer.java:64)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
问题分析
1 | public class WebContextLoaderListener implements ServletContextLinstener { |
tomcat停止时,有执行销毁线程池操作,但线程并没有关闭。实际是executorService.shutdown()这种关闭线程的方式存在问题;
- 调用shutdownNow()后,不可以再提交新的task(executorService.execute()操作),已经提交的将继续执行,当所有线程结束执行当前任务,executorService才会真正关闭。
- 调用shutdown()后,试图停止当前正在执行的task,并返回尚未执行的task列表。
解决方式
1 | @Override |
其他问题
阿里巴巴的JAva开发手册有这样一条规范:
【强制】 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2) CachedThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
Spring core包提供的ThreadPoolTaskExecutor对ThreadPoolExecutor进行了封装处理,项目中线程池配置如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<bean id="consumerThreadPool" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<!-- 线程池的名称前缀 -->
<property name="threadNamePrefix" value="issc-mq-message-consumer-threadpool-" />
<!-- 设置为守护线程 -->
<property name="daemon" value="true" />
<!-- 线程池维护线程的最少数量 -->
<property name="corePoolSize" value="5" />
<!-- 线程池维护线程的最大数量 -->
<property name="maxPoolSize" value="10" />
<!-- 缓存队列的长度 -->
<property name="queueCapacity" value="10" />
<!-- 允许的空闲时间 -->
<property name="keepAliveSeconds" value="90" />
<!-- 对拒绝task的处理策略 -->
<property name="rejectedExecutionHandler">
<bean class="java.util.concurrent.ThreadPoolExecutor$AbortPolicy" />
</property>
</bean>
ThreadPoolTaskExecutor的处理流程:
- 当线程数量小于corePoolSize,新建线程处理请求
- 当线程数量等于corePoolSize,把请求放入blockingQueue中,线程池中的空闲线程从blockingQueue中获取任务并处理
- 当blockingQueue满了,会新建线程处理请求,如果线程数量等于maxPoolSize,使用rejectedExecutionHandler做拒绝处理
- 当线程数量大于corePoolSize时,多于的线程会等待keepAliveSeconds时间,如果无请求处理就自行销毁
修改后代码如下,设置为守护线程后,tomcat关闭时无需手动关闭线程。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public class WebContextLoaderListener implements ServletContextLinstener {
private Logger log = LoggerFactory.getLogger(WebContextLoaderListener.class);
@Override
public void contextInitialized(ServletContextEvent sce){
try{
//启动事件消费任务
TaskExecutor consumerThreadPool= AppContext.getBean("consumerThreadPool");
if(consumerThreadPool != null){
Map<String, AbstractEventConsumer> eventConsumerList = AppContext.getBean(AbstractEventConsumer.class);
if(MapUtils.isNotEmpty(eventConsumerList)){
for (AbstractEventConsumer consumer : eventConsumerList.values()){
if(consumer != null){
executorService.execute(consumer);
}
}
}
}
}
catch(Exception){
log.error(*Log.toLog(ErrorCodeEnum.UNKNOW_ERROR.getCodeStr(), "WebContextLoaderListener init fail"), e);
}
}
}