1.定時器
1.1 含義
在Java中,定時器(Timer)是一個工具類,用于安排任務(Task)在指定時間後執行或以指定的時間間隔重複執行。它可以用于執行定時任務、定時排程和時間延遲等操作。
定時器(Timer)可以應用于許多場景,比如:
- 排程任務:當你需要按照預定時間執行任務時,可以使用定時器。例如,每天淩晨執行資料備份、定時生成報表、定時發送通知等。
- 逾時處理:當你需要處理某個操作的逾時情況時,可以使用定時器。例如,設定一個操作的逾時時間,如果在規定時間内未完成,則執行相應的逾時處理邏輯。
1.2 标準庫中的定時器
Java中的定時器:java.util.Timer,它的常用方法:
方法 | 描述 |
schedule(TimerTask task, Date time) | 安排在指定時間執行任務。 |
schedule(TimerTask task, long delay) | 安排在指定延遲時間後執行任務。 |
schedule(TimerTask task, long delay, long period) | 安排在指定延遲時間後以指定的時間間隔重複執行任務。 |
scheduleAtFixedRate(TimerTask task, Date firstTime, long period) | 安排在指定時間開始以固定的時間間隔重複執行任務。 |
scheduleAtFixedRate(TimerTask task, long delay, long period) | 安排在指定延遲時間後以固定的時間間隔重複執行任務。 |
cancel() | 取消定時器的所有任務。 |
purge() | 從定時器的任務隊列中删除所有已取消的任務。 |
java複制代碼public class Main {
public static void main(String[] args) {
Timer timer = new Timer();
//排程指定的任務在指定的延遲時間(3000ms)後執行。
timer.schedule(new TimerTask() {
//待執行的任務
@Override
public void run() {
System.out.println("hello");
}
},3000);
}
}
也可以一次注冊多個任務:
java複制代碼public class Main {
public static void main(String[] args) {
Timer timer = new Timer();
//在指定的延遲時間(1000ms)後執行。
timer.schedule(new TimerTask() {
//待執行的任務
@Override
public void run() {
System.out.println("任務1");
}
},1000);
//在指定的延遲時間(2000ms)後執行。
timer.schedule(new TimerTask() {
//待執行的任務
@Override
public void run() {
System.out.println("任務2");
}
},2000);
//在指定的延遲時間(3000ms)後執行。
timer.schedule(new TimerTask() {
//待執行的任務
@Override
public void run() {
System.out.println("任務3");
}
},3000);
}
}
2.簡單模拟實作定時器
2.1 實作思路
- 使用一個資料結構來儲存所有的任務,這些任務是根據時間的大小來進行先後執行的,是以這裡使用優先級隊列。由于這裡是多線程的環境,是以這裡采用PriorityBlockingQueue(優先級阻塞隊列),時間越小優先級越高。
- 我們需要使用一個線程來掃描定時器裡面的任務是否到達執行時間,由于我們采用的是優先級隊列資料結構,是以隻需掃描隊首元素。如果隊首還沒到執行時間,那麼後面的元素不可能到達執行時間。
- 任務用一個類MyTask來表示,這裡需要實作Comparable接口,因為它需要存入優先級隊列。其中的屬性:
java複制代碼//表示定時器中的任務
class MyTask implements Comparable<MyTask>{
//要執行的任務内容
private Runnable runnable;
//延遲時間
private long time;
public MyTask(Runnable runnable, long time) {
this.runnable = runnable;
this.time = time;
}
//為了便于後面的比較,需要提供 get 方法
public long getTime() {
return time;
}
//表示任務開始執行
public void run(){
this.runnable.run();
}
@Override
public int compareTo(MyTask o) {
return (int)(this.getTime() - o.getTime());
}
}
- 實作添加任務的方法schedule:
java複制代碼public class MyTimer {
//掃描線程
private Thread thread;
//優先級隊列(這裡為阻塞隊列)
private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
/**
* 這個方法是用來注冊(添加)任務的
* @param runnable 表示待執行的任務
* @param after 表示多少時間過後執行任務
*/
public void schedule(Runnable runnable,long after){
//添加任務,注意這裡的時間是 System.currentTimeMillis() + after
MyTask task = new MyTask(runnable,System.currentTimeMillis() + after);
queue.put(task);
}
}
- 添加一個線程來檢測隊首元素:
java複制代碼 //當建立對象的時候就直接開啟一個線程
public MyTimer(){
thread = new Thread(()->{
while(true){
//取出隊首,如果到時間了就執行。
try {
MyTask myTask = queue.take();
long curTime = System.currentTimeMillis();
if(curTime < myTask.getTime()){
//時間未到,不執行
queue.put(myTask);
}else {
//時間已到,執行
myTask.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
}
就這樣就完了嗎?其實不然,在上面代碼中while (true)轉的太快了, 造成了無意義的 CPU 浪費,如果第一個任務設定的是 1 min 之後執行某個邏輯,那麼在這一分鐘内 CPU 會一直存取隊首元素。是以這裡需要借助該對象的wait / notify來解決 while (true) 的忙等問題。
java複制代碼 public MyTimer(){
thread = new Thread(()->{
while(true){
//取出隊首,如果到時間了就執行。
try {
MyTask myTask = queue.take();
long curTime = System.currentTimeMillis();
if(curTime < myTask.getTime()){
queue.put(myTask);
//時間未到,不執行,這裡的 this 表示 MyTimer 對象
synchronized (this){
//阻塞一段時間
this.wait(myTask.getTime() - curTime);
}
}else {
//時間已到,執行
myTask.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
}
/**
* 這個方法是用來注冊(添加)任務的
* @param runnable 表示待執行的任務
* @param after 表示多少時間過後執行任務
*/
public void schedule(Runnable runnable,long after){
//添加任務,注意這裡的時間是 System.currentTimeMillis() + after
MyTask task = new MyTask(runnable,System.currentTimeMillis() + after);
queue.put(task);
synchronized(this){
this.notify();
}
}
修改 Timer 的 schedule 方法,每次有新任務到來的時候喚醒一下線程。(因為新插入的任務可能是需要馬上執行的)。
還沒結束!上面的代碼還是有缺陷的。假設當 thread 線程執行完 queue.take() 過後,myTask.getTime() - curTime 的值為 1 個小時。這時 CPU 排程了其它線程(假設為 t2) 執行, t2 線程調用 schedule 方法,延時時間為 30 分鐘,并調用 put 方法,随後再執行 notify 方法。然而這時 wait 方法還沒有執行,notify 相當于失效了。這時CPU再排程 thread 線程執行,但是 myTask.getTime() - curTime 的值本應是 30 分鐘(新添加了一個任務),但是實際上卻是 1 個小時。 這是因為queue.take()與wait不是原子操作,是以才導緻這個問題的發生,下面是改進後的代碼。
java複制代碼 public MyTimer(){
thread = new Thread(()->{
while(true){
//取出隊首,如果到時間了就執行。
try {
synchronized (this){
MyTask myTask = queue.take();
long curTime = System.currentTimeMillis();
if(curTime < myTask.getTime()){
queue.put(myTask);
//時間未到,不執行
//阻塞一段時間
this.wait(myTask.getTime() - curTime);
}else {
//時間已到,執行
myTask.run();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
}
2.2 完整代碼
java複制代碼//表示定時器中的任務
class MyTask implements Comparable<MyTask>{
//要執行的任務内容
private Runnable runnable;
//延遲時間
private long time;
public MyTask(Runnable runnable, long time) {
this.runnable = runnable;
this.time = time;
}
//為了便于後面的比較,需要提供 get 方法
public long getTime() {
return time;
}
//表示任務開始執行
public void run(){
this.runnable.run();
}
@Override
public int compareTo(MyTask o) {
return (int)(this.getTime() - o.getTime());
}
}
public class MyTimer {
//掃描線程
private Thread thread;
//優先級隊列
private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
public MyTimer(){
thread = new Thread(()->{
while(true){
//取出隊首,如果到時間了就執行。
try {
synchronized (this){
MyTask myTask = queue.take();
long curTime = System.currentTimeMillis();
if(curTime < myTask.getTime()){
queue.put(myTask);
//時間未到,不執行
//阻塞一段時間
this.wait(myTask.getTime() - curTime);
}else {
//時間已到,執行
myTask.run();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
}
/**
* 這個方法是用來注冊(添加)任務的
* @param runnable 表示待執行的任務
* @param after 表示多少時間過後執行任務
*/
public void schedule(Runnable runnable,long after){
//添加任務,注意這裡的時間是 System.currentTimeMillis() + after
MyTask task = new MyTask(runnable,System.currentTimeMillis() + after);
queue.put(task);
synchronized(this){
this.notify();
}
}
}