1.功能介紹
Spring框架提供了線程池和定時任務執行的抽象接口:TaskExecutor和TaskScheduler來支持異步執行任務和定時執行任務功能。同時使用框架自己定義的抽象接口來屏蔽掉底層JDK版本間以及Java EE中的線程池和定時任務處理的差異。
另外Spring還支持集成JDK內部的定時器Timer和Quartz Scheduler框架。
2.線程池的抽象:TaskExecutor
TaskExecutor涉及到的相關類圖如下:
TaskExecutor接口源代碼如下所示:
public interface TaskExecutor extends Executor {
/**
* Execute the given {@code task}.
* <p>The call might return immediately if the implementation uses
* an asynchronous execution strategy, or might block in the case
* of synchronous execution.
* @param task the {@code Runnable} to execute (never {@code null})
* @throws TaskRejectedException if the given task was not accepted
*/
@Override
void execute(Runnable task);
}
此接口和Executor幾乎完全一樣,只定義了一個接收Runnable參數的方法,據Spring官方介紹此接口最初是為了在其他組建中使用線程時,將JKD抽離出來而設計的。在Spring的一些其他組件中比如ApplicationEventMulticaster,Quartz都是使用TaskExecutor來作為線程池的抽象的。
3.Spring提供的TaskExecutor的實現類
org.springframework.core.task.SimpleAsyncTaskExecutor
此實現支持任務的異步執行,但是此實現沒有線程的復用,每次執行一個提交的任務時候都會新建一個線程,任務執行完成后會將線程關閉,最大並發數默認是沒有限制的,但是可以通過調用下面的方法來設置最大並發數。一般使用線程池來代替此實現,特別是執行一些生命周期很短的任務的時候。
public void setConcurrencyLimit(int concurrencyLimit) {
this.concurrencyThrottle.setConcurrencyLimit(concurrencyLimit);
}
Spring還提供了同步任務執行的實現類:
org.springframework.core.task.SyncTaskExecutor
此類中只有一個方法,代碼如下:
@Override
public void execute(Runnable task) {
Assert.notNull(task, "Runnable must not be null");
task.run();
}
此方法中直接調用傳入的Runable對象的run方法,因此在執行此方法的時候不會另外開啟新的線程,只是普通的方法調用,同步執行提交的Runable對象。
Spring有兩個線程池的實現類,分別為:SimpleThreadPoolTaskExecutor和ThreadPoolTaskExecutor,其中當我們有Quarts和非Quarts共享同一個線程池的需求的時候使用SimpleThreadPoolTaskExecutor,除了這種情況,我們一般是使用
ThreadPoolTaskExecutor,此實現可以通過屬性注入來配置線程池的相關配置。 ThreadPoolTaskExecutor中屬性注入的源碼如下:此配置可以在運行期修改,代碼中修改過程使用了同步控制。
/**
* Set the ThreadPoolExecutor's core pool size.
* Default is 1.
* <p><b>This setting can be modified at runtime, for example through JMX.</b>
*/
public void setCorePoolSize(int corePoolSize) {
synchronized (this.poolSizeMonitor) {
this.corePoolSize = corePoolSize;
if (this.threadPoolExecutor != null) {
this.threadPoolExecutor.setCorePoolSize(corePoolSize);
}
}
}
4.TaskExecutor使用Demo
首先定義一個任務如下所示:
public class DataSimulation implements Runnable{
private HourAverageValueDao hourAverageValueDao;
@Override
public void run() {
Random random = new Random();
AverageValue averageValue = new AverageValue();
averageValue.setAverageVaule(random.nextInt(100));
averageValue.setCreatedTime(new Date());
hourAverageValueDao.insert(averageValue);
}
}
此任務中產生一個隨機數,並封裝成一個類對象,並將此數據插入到數據庫中。
然后需要定一個類,使用TaskExecutor,代碼如下:
public class DataFacotory {
private TaskExecutor executor;
public TaskExecutor getExecutor() {
return executor;
}
public void setExecutor(TaskExecutor executor) {
this.executor = executor;
}
public void dataFactory(){
for (int i =0; i < 10; i++){
executor.execute(new DataSimulation());
}
}
}
此類中定義了TaskExecutor的屬性,並定一個方法,此方法中提交10個任務到TaskExecutor,下面只需配置Spring文件,注入TaskExecutor就可以實現線程池的使用。配置文件如下所示:
<bean id = "taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="corePoolSize" value = "5"></property>
<property name = "maxPoolSize" value="10"></property>
<property name="queueCapacity" value="25"></property>
</bean>
完成配置后即可使用此線程池。Spring提供的線程池可以通過配置文件配置線程池的配置,相比JDk自帶的線程池是一個很大的優勢。
5.為什么使用線程池
1.通過使用線程池來實現線程的復用,減少線程創建和銷毀的開銷
2.將執行線程的任務交給線程池來操作,一定意義上實現了解耦
3.使用線程池可以控制任務的最大並發數目,這個在防止內存溢出以及並發優化方面有很重要的作用。
6.定時任務抽象類:TaskScheduler
TaskScheduler接口源代碼如下:
public interface TaskScheduler {
//通過觸發器來決定task是否執行
ScheduledFuture schedule(Runnable task, Trigger trigger);
//在starttime的時候執行一次
ScheduledFuture schedule(Runnable task, Date startTime);
從starttime開始每個period時間段執行一次task
ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, long period);
每隔period執行一次
ScheduledFuture scheduleAtFixedRate(Runnable task, long period);
從startTime開始每隔delay長時間執行一次
ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, long delay);
每隔delay時間執行一次
ScheduledFuture scheduleWithFixedDelay(Runnable task, long delay);
}
指定開始時間的接口,如果時間已經是過去的某一時間點,則此任務會馬上執行一次。以上幾種執行方式傳入Trigger的方式是用的最多的,Trigger接口中只定義了一個方法:
Date nextExecutionTime(TriggerContext triggerContext);
其中參數類型TriggerContext的定義如下:
public interface TriggerContext {
/**
* Return the last <i>scheduled</i> execution time of the task,
* or {@code null} if not scheduled before.
*/
Date lastScheduledExecutionTime();
/**
* Return the last <i>actual</i> execution time of the task,
* or {@code null} if not scheduled before.
*/
Date lastActualExecutionTime();
/**
* Return the last completion time of the task,
* or {@code null} if not scheduled before.
*/
Date lastCompletionTime();
}
提供了獲取上一次任務執行信息的接口。我們通過實現Trigger接口可以實現自定義觸發器來執行執行task。當然Spring也提供了兩個默認的實現類:PeriodicTrigger和CronTrigger。
7.TaskScheduler定時任務Demo
首先在Spring配置文件中啟用注解配置如下:
<task:annotation-driven scheduler="myScheduler"/> //指定scheduler屬性是可選項,不添加也可以正常使用
<task:scheduler id="myScheduler" pool-size="10"/>
然后創建service,並在service中使用@Scheduled注解創建定時任務,代碼如下:
@Component
public class SchedulerPoolTest {
@Scheduled(cron = "0 * * * * ?")
public void task1(){
System.out.println("test");
Thread thread = Thread.currentThread();
System.out.println("ThreadName:" + thread.getName() + ",id:" + thread.getId() + ",group:" + thread.getThreadGroup());
}
@Scheduled(fixedDelay = 5000)
public void task2(){
System.out.println("test");
Thread thread = Thread.currentThread();
System.out.println("ThreadName:" + thread.getName() + ",id:" + thread.getId() + ",group:" + thread.getThreadGroup());
}
}
只是添加以上內容可能還不能正常執行task,還需要注意以下兩點:
1.必須將SchedulerPoolTest類包含在spring所掃描的包里面
配置如下:
<context:component-scan base-package="com.zjut.task" />
2.需要在web.xml中添加spring配置文件的監聽器,代碼如下:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:spring-task.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
添加以上內容后,啟動服務器,將會定時執行任務。
8.Cron表達式
Cron表達式由6個字符串組成,每個字符串分別代表:
{秒} {分} {時} {日} {月} {周}
其中每個字符串所允許的取值范圍為:
字段名 允許的值 允許的特殊字符
秒 0-59 , - * /
分 0-59 , - * /
小時 0-23 , - * /
日 1-31 , - * ? / L W C
月 1-12 or JAN-DEC , - * /
周幾 1-7 or SUN-SAT , - * ? / L C #
常用的Cron表達式:
"0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2:44觸發
"0 0/5 14,18 * * ?" 在每天下午2點到2:55期間和下午6點到6:55期間的每5分鍾觸發
"15-30/5 * * * * ?" 每分鍾的15秒到30秒之間開始觸發,每隔5秒觸發一次
"0 15 10 ? * 5#3" 每個月第三周的星期四的10點15分0秒觸發任務
注:問號是用於避免日和周的設定由沖突而用的,當其中一個設置了具體的值,另外一個必須使用?。另外推薦一個Cron表達式生成的鏈接:http://www.cronmaker.com/
9.@Async注解
Async注解提供了異步調用方法的功能,當調用由此注解的方法的時候方法調用者會馬上返回而不會等待調用的方法執行完成,被調用的方法會從線程池中分配一個線程來執行此方法。
10.Spring定時任務中並發執行的問題
同一個任務,當上一個任務沒有執行完成的時候,新的任務不會執行。
不同任務的情況下:TODO...