前兩天vos線上服務遇到一個問題,定時任務突然全部停下來不跑了。看日志也沒發現什么明顯的異常輸出,加上比較忙,所以干脆直接使用重啟大法。沒想到過了兩天突然發現定時任務又全部停了,恰好正在趕需求,vos又是一個公司內部使用的系統,優先級較低,索性繼續重啟等有空了再來看。然后過了1天多定時任務?掛了。只能暫時放下手頭上的事看一下到底是什么導致的這個問題。
由于日志未輸出任何異常信息,所以要解決該問題只能靠分析代碼了。由于vos系統使用的定時任務框架是spring schduler,所以打算了解一下spring schduler的運行原理。
一、spring schduler 的運行原理
一般我們要使用schuduler,都會往spring的配置文件中加入下面這行。
<task:annotation-driven />
然后通過xml或者注解@Scheduled的方式配置具體的定時任務。下面我們圍繞注解的方式講解一下spring schduler是怎么解析和運行配置的定時任務的。
1. <task:annotation-driven />配置解析
spirng在解析配置文件的時候遇到<task:annotation-driven />的時候,會往spring容器中注入 ScheduledAnnotationBeanPostProcessor 對象實例。具體的解析過程這里就不詳述了。感興趣的可以自行百度或者看一下下面這個鏈接。
http://blog.csdn.net/tt50335971/article/details/52055755
2. ScheduledAnnotationBeanPostProcessor類
ScheduledAnnotationBeanPostProcessor 是BeanPostProcessor的子類。在spring bean的生命周期中,所有的bean構造完后都會執行postProcessAfterInitialization()
方法。因此,spring容器注入ScheduledAnnotationBeanPostProcessor對象后,每次容器構造bean后都會執行ScheduledAnnotationBeanPostProcessor#postProcessAfterInitialization()
方法。下面我們來看下這個方法大體長什么樣:
public Object postProcessAfterInitialization(final Object bean, String beanName) {
//獲取具體的類
final Class<?> targetClass = AopUtils.getTargetClass(bean);
//獲取并遍歷該類的所有方法
ReflectionUtils.doWithMethods(targetClass, new MethodCallback() {
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
//獲取該方法的Scheduled注解
Scheduled annotation = AnnotationUtils.getAnnotation(method, Scheduled.class);
//如果方法沒有Scheduled注解,那就沒有必要執行了
if (annotation != null) {
try {
///省略一些代碼
//獲取Scheduled注解的cron屬性值
String cron = annotation.cron();
if (!"".equals(cron)) {
Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
processedSchedule = true;
if (embeddedValueResolver != null) {
cron = embeddedValueResolver.resolveStringValue(cron);
}
//往registrar加一個定時任務
registrar.addCronTask(new CronTask(runnable, cron));
}
// 下面代碼省略,具體讀者可以自行打開IDE查看這個類
}
}
});
3. ScheduledTaskRegistrar類
從上面可以看出,ScheduledAnnotationBeanPostProcessor會遍歷spring 容器中所有bean的所有方法,如果發現有Scheduled注解的,就解析然后往registrar加入一個CronTask對象。所以我們再接著來分析registrar。從代碼可以很直觀看到,它對應的類是ScheduledTaskRegistrar。我們來看一下它一些變量的定義
private TaskScheduler taskScheduler;
private ScheduledExecutorService localExecutor;
private List<TriggerTask> triggerTasks;
private List<CronTask> cronTasks;
private List<IntervalTask> fixedRateTasks;
private List<IntervalTask> fixedDelayTasks;
private final Set<ScheduledFuture<?>> scheduledFutures = new LinkedHashSet<ScheduledFuture<?>>();
在前面的代碼中,spring將各個Task加入到各種List中。但是各種Task只是一個抽象的類,java程序并不會識別他們并定時的執行任務。那是什么時候講Task轉化成真正運行著的定時任務呢?
這里我們先來看下ScheduledTaskRegistrar的初始化。由于它實現了InitializingBean接口,所以它構造完后會執行對應的afterPropertiesSet
方法。
public void afterPropertiesSet() {
scheduleTasks();
}
protected void scheduleTasks() {
long now = System.currentTimeMillis();
if (this.taskScheduler == null) {
//構造一個單線程的線程池。所以所有的定時任務其實都是一個線程在跑。
this.localExecutor = Executors.newSingleThreadScheduledExecutor();
//具體定時任務的實現類,下面會講
this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
}
if (this.triggerTasks != null) {
for (TriggerTask task : triggerTasks) {
this.scheduledFutures.add(this.taskScheduler.schedule(
task.getRunnable(), task.getTrigger()));
}
}
if (this.cronTasks != null) {
for (CronTask task : cronTasks) {
//遍歷task集合,然后將返回的scheduledFuture加入到scheduledFutures集合中
this.scheduledFutures.add(this.taskScheduler.schedule(
task.getRunnable(), task.getTrigger()));
}
}
//為了篇幅,繼續省略一些代碼
}
這里我們看到了這個類怎么構造ScheduledFuture并加入到scheduledFutures集合中的。下面繼續來看構造ScheduledFuture的關鍵類ConcurrentTaskScheduler
4. ConcurrentTaskScheduler類
首先我們先隨便來看一個方法:
public ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, long period) {
long initialDelay = startTime.getTime() - System.currentTimeMillis();
try {
//this.scheduledExecutor就是前面定義的那個單線程池
//errorHandlingTask(task, true) 是對任務進行封裝,如果任務拋出異常,我們可以按自己選擇的方式來處理異常。第二個參數為true表示就算發生異常也會繼續執行下一次任務。
return this.scheduledExecutor.scheduleAtFixedRate(
errorHandlingTask(task, true), initialDelay, period, TimeUnit.MILLISECONDS);
}
catch (RejectedExecutionException ex) {
throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex);
}
}
這里可以看出,spring scheduler底層其實用的還是java的ScheduledExecutorService配置定時任務的。
再來看下最常用的cron方式配置的定時任務是怎么下發的:
public ScheduledFuture schedule(Runnable task, Trigger trigger) {
try {
ErrorHandler errorHandler =
(this.errorHandler != null ? this.errorHandler : TaskUtils.getDefaultErrorHandler(true));
//將構造ScheduledFuture的權利交給了ReschedulingRunnable類
return new ReschedulingRunnable(task, trigger, this.scheduledExecutor, errorHandler).schedule();
}
catch (RejectedExecutionException ex) {
throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex);
}
}
5. ReschedulingRunnable類
繼續跟蹤查看ReschedulingRunnable類的幾個重要方法:
public ScheduledFuture schedule() {
synchronized (this.triggerContextMonitor) {
//根據cron表達式計算出下一次要執行的時間
this.scheduledExecutionTime = this.trigger.nextExecutionTime(this.triggerContext);
if (this.scheduledExecutionTime == null) {
return null;
}
long initialDelay = this.scheduledExecutionTime.getTime() - System.currentTimeMillis();
//用java自帶的ScheduledExecutorService下發一個延遲任務(過xxxms后執行)
this.currentFuture = this.executor.schedule(this, initialDelay, TimeUnit.MILLISECONDS);
return this;
}
}
@Override
public void run() {
//真正開始跑
Date actualExecutionTime = new Date();
//執行父類的run方法
super.run();
Date completionTime = new Date();
synchronized (this.triggerContextMonitor) {
this.triggerContext.update(this.scheduledExecutionTime, actualExecutionTime, completionTime);
//如果沒有收到結束通知,繼續看下一次什么時候執行
if (!this.currentFuture.isCancelled()) {
schedule();
}
}
}
這是ReschedulingRunnable類的兩個關鍵方法,schedule返回一個ScheduledFuture對象。他其實就是分析cron表示式后獲取到下一次要執行的時間,然后交給ScheduledExecutorService去下發。
到執行的時間后,會執行run方法。run方法執行到后面,會繼續調用schedule()
方法。然后再計算下次執行的時候,然后下發。這兩個方法一直循環,就構成了一個定時任務。
6.1將任務調度器緩存多線程
前面我們可以看到,這里的調度任務是多線程執行的,spring也提供了將單線程換成多線程執行的地方。我們是先看源碼,還是ScheduledAnnotationBeanPostProcessor
這個類。
public void onApplicationEvent(ContextRefreshedEvent event) {
if (event.getApplicationContext() != this.applicationContext) {
return;
}
//獲取所有我們自己定義的SchedulingConfigurer接口實現類
Map<String, SchedulingConfigurer> configurers =
this.applicationContext.getBeansOfType(SchedulingConfigurer.class);
if (this.scheduler != null) {
this.registrar.setScheduler(this.scheduler);
}
//這里會將ScheduledTaskRegistrar傳入這個方法,這樣我們可以定義一個類,繼承SchedulingConfigurer接口,到這里拿到ScheduledTaskRegistrar就可以修改線程池的實現了
for (SchedulingConfigurer configurer : configurers.values()) {
configurer.configureTasks(this.registrar);
}
//后面省略
}
spring的生命周期中,會自動調用onApplicationEvent這個方法。接著拿到SchedulingConfigurer的所有實現類,調用configureTasks方法,同時還會傳入ScheduledTaskRegistrar對象。這個ScheduledTaskRegistrar實例是spring實現任務調度的關鍵所在!!!所以只要我們修改一下它的線程池實現(默認是單線程池),就可以將任務調度變成多線程了。下面上代碼:
@Component
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
threadPoolTaskScheduler.setPoolSize(10);
threadPoolTaskScheduler.initialize();
taskRegistrar.setTaskScheduler(threadPoolTaskScheduler);
taskRegistrar.getScheduler().schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
}, new CronTrigger("0 0/5 * * * ?"));
}
}
這里將調度的線程池調到了10個線程。
6.2 手動注入ThreadPoolTaskScheduler來實現多線程
<task:scheduler id="scheduler" pool-size="10"/>
在spring配置文件中加上上面的代碼,spring啟動的時候會往容器注入一個ThreadPoolTaskScheduler 實例。這個實例線程池大小配置為10。這樣,spring啟動的時候就會自動選擇這個實例來作為ScheduledTaskRegistrar的TaskScheduler的實現。如果沒在容器里面發現任何TaskScheduler實例,就會new一個單線程的線程池。
if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) {
Map<String, ? super Object> schedulers = new HashMap<String, Object>();
schedulers.putAll(applicationContext.getBeansOfType(TaskScheduler.class));
schedulers.putAll(applicationContext.getBeansOfType(ScheduledExecutorService.class));
//沒在容器中發現任何定時任務的線程池,就用默認的單線程池
if (schedulers.size() == 0) {
// do nothing -> fall back to default scheduler
}
//發現了一個,就直接用它了
else if (schedulers.size() == 1) {
this.registrar.setScheduler(schedulers.values().iterator().next());
}
//如果超過兩個會報錯哦!
else if (schedulers.size() >= 2){
throw new IllegalStateException(
"More than one TaskScheduler and/or ScheduledExecutorService " +
"exist within the context. Remove all but one of the beans; or " +
"implement the SchedulingConfigurer interface and call " +
"ScheduledTaskRegistrar#setScheduler explicitly within the " +
"configureTasks() callback. Found the following beans: " + schedulers.keySet());
}
}
7. 結論
分析到這里我們其實可以得出一些結論:
- spring scheduler底層使用的還是juc中的ScheduledExecutorService類。
- 默認情況下,所有的定時任務都是放在一個線程跑的。也就是說,如果這個線程執行某個任務崩潰了或者執行某個任務的時候卡住了,那其他的定時任務也就都無法執行了。
- 通過配置可以將單線程執行所有調度變成多線程執行所有調度任務。
- 由于spring使用javaScheduledExecutorService配置定時任務的時候對任務做了封裝,所以即使執行的任務發生了我們沒有捕獲的異常,也不會導致線程崩潰。因此,導致spring scheduler定時任務全部停止的原因很可能是因為執行某個定時任務的時候卡住了。
總結到這里,再聯想到最近往vos加了個定時任務,里面有用到httpclient發送http請求。猜想可能是這里卡住了。所以接著去了解了下apache的httpclient這個包。
二、httpclient 輸入流未設置超時時間引起的阻塞
網上查了一下,httpclient的超時其實有兩種,一種是連接超時(connect timeout),一種是讀超時(socket timeout)。
httpclient的默認讀時間是設置為0,也就是永遠不超時的意思,這樣就導致讀的時候因為出現某個問題線程阻塞在讀這里。通過jstack命令查看線程堆棧我們也可以確認線程確實阻塞在socket讀那里了。
"org.springframework.jms.listener.DefaultMessageListenerContainer#7-1" prio=10 tid=0x00007f345127d800 nid=0x5b4f0 runnable [0x00007f34753d1000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.read(SocketInputStream.java:150)
at java.net.SocketInputStream.read(SocketInputStream.java:121)
at org.apache.http.impl.io.AbstractSessionInputBuffer.fillBuffer(AbstractSessionInputBuffer.java:130)
at org.apache.http.impl.io.SocketInputBuffer.fillBuffer(SocketInputBuffer.java:127)
at org.apache.http.impl.io.AbstractSessionInputBuffer.readLine(AbstractSessionInputBuffer.java:233)
at org.apache.http.impl.io.ChunkedInputStream.getChunkSize(ChunkedInputStream.java:220)
at org.apache.http.impl.io.ChunkedInputStream.nextChunk(ChunkedInputStream.java:183)
at org.apache.http.impl.io.ChunkedInputStream.read(ChunkedInputStream.java:152)
at org.apache.http.conn.EofSensorInputStream.read(EofSensorInputStream.java:138)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:283)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:325)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:177)
- locked <0x000000070346ce70> (a java.io.InputStreamReader)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.Reader.read(Reader.java:140)
at org.apache.http.util.EntityUtils.toString(EntityUtils.java:161)
我們可以通過設置讀超時時間來解決這個問題:
HttpClient client = new HttpClient();
client.getHttpConnectionManager().getParams().setConnectionTimeout(5000);
client.getHttpConnectionManager().getParams().setSoTimeout(5000);
這里設置了5秒的連接超時時間和讀超時時間,也就是說,阻塞5秒后如果還沒有得到響應,就會放棄繼續讀,讓線程可以往下執行。