應用場景
(1) 鬧鐘程序或任務提醒,指定時間叫牀或在指定日期提醒還信用卡。
(2) 監控系統,每隔一段時間採集下系統數據,對異常事件報警
(3) 統計系統,一般凌晨一定時間統計昨日的各種數據指標。
實現定時任務的兩種方式
(1) 使用java.util包中的Timer和TimerTask。
(2) 使用Java併發包中的ScheduleExecutorService。
Timer和TimerTask
基本用法
TimerTask 表示一個定時任務,它是一個抽象類,實現了Runable,具體的定時任務需要繼承該類,實現了run方法。Timer是一個具體類,它負責定時任務的調度和執行,主要方法有:
//在當前時間延時delay 毫秒後運行任務task
public void schedule(TimerTask task, long delay) {
if (delay < 0)
throw new IllegalArgumentException("Negative delay.");
sched(task, System.currentTimeMillis()+delay, 0);
}
// 在指定絕對時間time 執行任務task
public void schedule(TimerTask task, Date time) {
sched(task, time.getTime(), 0);
}
//固定延時重複執行,第一次計劃執行時間爲firstTime,後一次的計劃執行時間爲前一次“實際”執行時間加上period
public void schedule(TimerTask task, Date firstTime, long period) {
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
sched(task, firstTime.getTime(), -period);
}
// 固定延時重複執行,第一次執行時間爲當前時間加上delay
public void schedule(TimerTask task, long delay, long period) {
if (delay < 0)
throw new IllegalArgumentException("Negative delay.");
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
sched(task, System.currentTimeMillis()+delay, -period);
}
//固定頻率重複執行,第一次執行時間爲當前時間加上delay
public void scheduleAtFixedRate(TimerTask task, long delay, long period) {
if (delay < 0)
throw new IllegalArgumentException("Negative delay.");
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
sched(task, System.currentTimeMillis()+delay, period);
}
//固定頻率重複執行,第一次計劃執行時間爲firstTime,後一次的計劃執行時間爲前一次“實際”執行時間加上period
public void scheduleAtFixedRate(TimerTask task, Date firstTime,
long period) {
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
sched(task, firstTime.getTime(), period);
}
需要注意固定延時與固定頻率的區別,二者都是重複執行,但後一次任務執行相對的時間是不一樣的,對於固定延時,它是基於上次任務的“實際”執行時間來算的,如果由於某種原因,上次任務延時了,則本次任務也會延時,而固定頻率會盡量補夠運行次數。
另外,需要注意的是,如果第一次計劃執行的時間firstTime是一個過去的時間,則任務會立即運行,對於固定延時的任務,下次任務會基於第一次執行時間計算,而對於固定頻率的任務,則會從firstTime開始算,有可能加上period後還是一個過去時間,從而連續運行很多次,直到時間超過當前時間。
基本示例
package com.claa.javabasic.TimerDemo;
import java.util.Timer;
import java.util.TimerTask;
/**
* @Author: claa
* @Date: 2020/05/24 08:58
* @Description:
*/
public class BasicTimer {
static class DelayTask extends TimerTask{
@Override
public void run() {
System.out.println("delayed task");
}
}
public static void main(String[] args) throws InterruptedException{
Timer timer = new Timer();
timer.schedule(new DelayTask(),1000);
Thread.sleep(2000);
timer.cancel(); // 取消所有定時任務
}
}
package com.claa.javabasic.TimerDemo;
import java.util.Timer;
import java.util.TimerTask;
/**
* @Author: claa
* @Date: 2020/05/24 09:20
* @Description: 固定延時示例
* 有兩個定時任務,第一個運行一次,但耗時5秒,第二個是重複執行,1秒一次,
* 第一個先運行,運行該程序,會發現,第二個任務只有在第一個任務運行結束後纔開始運行,運行後1秒1次
*/
public class TimerFixedDelay {
static class LongRuningTask extends TimerTask {
@Override
public void run() {
try{
Thread.sleep(5000);
}catch(InterruptedException e){
}
System.out.println("long runing finished");
}
}
static class FixedDelayTask extends TimerTask{
@Override
public void run() {
System.out.println(System.currentTimeMillis());
}
}
public static void main(String[] args) throws InterruptedException{
Timer timer = new Timer();
timer.schedule(new LongRuningTask(),10);
timer.schedule(new FixedDelayTask(),100,1000);
}
}
package com.claa.javabasic.TimerDemo;
import java.util.Timer;
import java.util.TimerTask;
/**
* @Author: claa
* @Date: 2020/05/24 09:32
* @Description:固定頻率
* 第二個任務同樣只有在第一個任務運行結束後纔會運行,但它會把之前沒有運行的次數補過來,一下子運行5次
*/
public class TimerFixedRate {
static class LongRuningTask extends TimerTask {
@Override
public void run() {
try{
Thread.sleep(5000);
}catch(InterruptedException e){
}
System.out.println("long runing finished");
}
}
static class FixedRateTask extends TimerTask {
@Override
public void run() {
System.out.println(System.currentTimeMillis());
}
}
public static void main(String[] args) throws InterruptedException{
Timer timer = new Timer();
timer.schedule(new LongRuningTask(),10);
timer.scheduleAtFixedRate(new FixedRateTask(),100,1000);
}
}
基本原理
Timer 內部主要由任務隊列和Timer線程兩部分組成。任務隊列是一個基於堆實現的優先級隊列,按照下次執行的時間安排優先級。Timer線程負責執行所有的定時任務,需要強調的是,一個Timer對象只有一個Timer線程,所以,上面的例子,任務會被延遲。
Timer線程主體是一個循環,從隊列中獲取任務,如果隊列中有任務且計劃執行時間小於等於當前時間 ,就執行它。如果隊列中沒有任務或第一個任務延時還沒到,就睡眠。如果睡眠過程中隊列上添加了新任務且新任務是第一個任務,Timer線程會被喚醒,重新進行檢查。
在執行任務之前,Timer線程判斷任務是否爲週期任務,如果是,就設置下次執行的時間並添加到優先級隊列,對於固定延時的任務,下次執行時間爲當前時間加上period, 對於固定頻率的任務,下次執行時間爲上次計劃執行時間加上period。
需要強調的是,下次任務的計劃是在執行當前任務之前就做出的,對於固定延時的任務,延時相對的是任務執行前的當前時間,而不是任務執行後,這與後面講到的ScheduledExecutorService 的固定延時計算方法 不同,後者更合乎一般的期望。對於固定頻率的任務 ,延時相對的是最先的計劃,所以,很有可能會出現前面例子中一下子執行很多次任務的情況。
死循環
一個Timer對象只有一個Timer線程,這意味着,定時任務不能耗時太長,更不能是無限循環。
package com.claa.javabasic.TimerDemo;
import java.util.Timer;
import java.util.TimerTask;
/**
* @Author: claa
* @Date: 2020/05/24 10:16
* @Description:Timer 循環示例
*/
public class EndLessLoopTimer {
static class LoopTask extends TimerTask{
@Override
public void run() {
while(true){
try{
// 模擬執行任務
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
// 永遠沒有機會執行
static class ExampleTask extends TimerTask{
@Override
public void run() {
System.out.println("hello");
}
}
public static void main(String[] args) throws InterruptedException{
Timer timer = new Timer();
timer.schedule(new LoopTask(),10);
timer.schedule(new ExampleTask(),100);
}
}
異常處理
關於Timer線程,在執行任何一個任務的run方法時,一旦拋出異常,Timer線程就會退出,從而所有定時任務都會被取消。
package com.claa.javabasic.TimerDemo;
import java.util.Timer;
import java.util.TimerTask;
/**
* @Author: claa
* @Date: 2020/05/24 10:33
* @Description:
*/
public class TimerException {
static class TaskA extends TimerTask {
@Override
public void run() {
System.out.println("task A");
}
}
static class TaskB extends TimerTask{
@Override
public void run() {
try{
System.out.println("task B");
throw new RuntimeException();
}catch (RuntimeException e){
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Timer timer = new Timer();
timer.schedule(new TaskA(),1,1000);
timer.schedule(new TaskB(),2000,1000);
}
}
如果希望各個定時任務不互相干擾,一定要在run方法內捕獲所有異常。
注意點
(1)後臺只有一個線程在運行;
(2)) 固定頻率的任務被延遲後,可能會立即執行多次,將次數補夠;
(3)固定延時任務的延時相對的是任務執行前的時間;
(4))不要在定時任務中使用無限循環;
(5) 一個定時任務的未處理異常會導致所有定時任務被取消。
ScheduledExecutorService
基本用法
public interface ScheduledExecutorService extends ExecutorService {
// 單次執行,在指定延時delay 後運行command
public ScheduledFuture<?> schedule(Runnable command,
long delay, TimeUnit unit);
// 單次執行,在指定延時delay 後運行callable
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
long delay, TimeUnit unit);
// 固定頻率重複執行
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
// 固定延時重複執行
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
}
對於固定頻率的任務,第一次執行時間爲initialDelay後,第二次爲initialDelay+period,第三次爲initialDelay+2*period,以此類推。不過,對於固定延時的任務,它是從任務執行後開始算的,第一次爲initialDelay後,第二次爲第一次任務執行結束後再加上delay
ScheduledExecutorService的主要實現類是ScheduledThreadPoolExecutor,它是線程池ThreadPoolExecutor的子類,是基於線程池實現的,它的主要構造方法是:
public ScheduledThreadPoolExecutor(int corePoolSize)
此外,還有構造方法可以接受參數ThreadFactory和RejectedExecutionHandler。
它的任務隊列是一個無界的優先級隊列,所以最大線程數對它沒有作用,即使core-PoolSize設爲0,它也會至少運行一個線程。
基本示例
由於可以有多個線程執行定時任務,一般任務就不會被某個長時間運行的任務所延遲了。
package com.claa.javabasic.ScheduledDemo;
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* @Author: claa
* @Date: 2020/05/24 11:23
* @Description:
*/
public class ScheduledFixedDelay {
static class LongRuningTask extends TimerTask {
@Override
public void run() {
try{
Thread.sleep(5000);
}catch(InterruptedException e){
}
System.out.println("long runing finished");
}
}
static class FixedDelayTask extends TimerTask{
@Override
public void run() {
System.out.println(System.currentTimeMillis());
}
}
public static void main(String[] args) throws InterruptedException {
ScheduledExecutorService timer = Executors.newScheduledThreadPool(10);
timer.schedule(new LongRuningTask(),10, TimeUnit.MILLISECONDS);
timer.scheduleWithFixedDelay(new FixedDelayTask(),100,1000,TimeUnit.MILLISECONDS);
}
}
與Timer不同,單個定時任務的異常不會再導致整個定時任務被取消,即使後臺只有一個線程執行任務。
package com.claa.javabasic.ScheduledDemo;
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* @Author: claa
* @Date: 2020/05/24 11:31
* @Description:
*/
public class ScheduleException {
static class TaskA extends TimerTask {
@Override
public void run() {
System.out.println("task A");
}
}
static class TaskB extends TimerTask{
@Override
public void run() {
try{
System.out.println("task B");
throw new RuntimeException();
}catch (RuntimeException e){
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor();
timer.scheduleWithFixedDelay(new TaskA(),0,1, TimeUnit.SECONDS);
timer.scheduleWithFixedDelay(new TaskB(),2,1, TimeUnit.SECONDS);
}
}
定時任務TaskB被取消了,但TaskA不受影響,即使它們是由同一個線程執行的。不過,需要強調的是,與Timer不同,沒有異常被拋出,TaskB的異常沒有在任何地方體現。所以,與Timer中的任務類似,應該捕獲所有異常。
基本原理
ScheduledThreadPoolExecutor的實現思路與Timer基本是類似的,都有一個基於堆的優先級隊列,保存待執行的定時任務,
它的主要不同是:
(1)它的背後是線程池,可以有多個線程執行任務。
(2)它在任務執行後再設置下次執行的時間,對於固定延時的任務更爲合理。
(3)任務執行線程會捕獲任務執行過程中的所有異常,一個定時任務的異常不會影響其他定時任務,不過,發生異常的任務(即使是一個重複任務)不會再被調度。