Java Timer定時任務源碼分析

前言

Java 提供的java.util.Timer類可以用來執行延時任務,任務可以只執行一次,也可以周期性的按照固定的速率或延時來執行。

實現一個延時任務調度器,核心有兩點:

  1. 如何存儲延時任務
  2. 如何調度執行延時任務

源碼分析

TimerTask

延時任務的核心屬性有兩個:

  1. 任務的邏輯(任務要干什么)
  2. 任務計劃執行時間

Java 把延時任務封裝成java.util.TimerTask類,實現自 Runnable,可以被線程執行。

public abstract class TimerTask implements Runnable {int state = VIRGIN;// 默認值,表示任務還沒被安排調度執行static final int VIRGIN = 0;// 任務入隊,等待調度static final int SCHEDULED   = 1;// 任務執行中static final int EXECUTED    = 2;// 任務取消static final int CANCELLED   = 3;long nextExecutionTime;long period = 0;
}

屬性說明:

  • state 任務的狀態,例如 執行中、已取消等
  • nextExecutionTime 任務下次執行的時間
  • period 任務重復執行的時間周期,正值表示固定速率執行,負值表示固定延時執行,0表示非重復任務

Timer

Java 把延時任務調度器封裝成java.util.Timer

public class Timer {private final TaskQueue queue = new TaskQueue();private final TimerThread thread = new TimerThread(queue);
}

屬性說明:

  • queue 任務優先級隊列,基于最小堆實現,隊頭永遠是最早要執行的任務
  • thread 任務調度執行線程,單線程執行

提交延時任務,最終會調用sched(),給 task 賦值任務的執行時間,狀態等信息,然后加入到隊列,如果隊頭就是自己,那么就要喚醒線程輪詢隊列調度執行。

private void sched(TimerTask task, long time, long period) {if (time < 0)throw new IllegalArgumentException("Illegal execution time.");if (Math.abs(period) > (Long.MAX_VALUE >> 1))period >>= 1;synchronized(queue) {if (!thread.newTasksMayBeScheduled)throw new IllegalStateException("Timer already cancelled.");// 搶鎖synchronized(task.lock) {if (task.state != TimerTask.VIRGIN)throw new IllegalStateException("Task already scheduled or cancelled");// 設置任務的執行時間,重復執行的時間周期,狀態改為已調度task.nextExecutionTime = time;task.period = period;task.state = TimerTask.SCHEDULED;}// 入隊queue.add(task);// 如果隊頭是自己,喚醒線程調度執行if (queue.getMin() == task)queue.notify();}
}

TimerThread

執行延時任務的線程被封裝成java.util.TimerThread類,繼承自 Thread,內部持有任務隊列的引用。

class TimerThread extends Thread {private TaskQueue queue;
}

線程執行會調用mainLoop(),不斷輪詢任務隊列,如果隊列是空的,線程就 wait,等待外部提交延時任務將自己喚醒。如果隊列非空,就判斷隊頭節點的執行時間是否已到,時間到了就立即執行任務,否則就 wait 指定時間。如果任務是一次性的,就把它從堆中移除;如果任務是重復執行的,就再重新添加到堆中。

private void mainLoop() {while (true) {try {TimerTask task;boolean taskFired;synchronized(queue) {// 隊列空就waitwhile (queue.isEmpty() && newTasksMayBeScheduled)queue.wait();if (queue.isEmpty())break; // Queue is empty and will forever remain; die// 獲取隊頭節點long currentTime, executionTime;task = queue.getMin();synchronized(task.lock) {if (task.state == TimerTask.CANCELLED) {queue.removeMin();continue;  // No action required, poll queue again}currentTime = System.currentTimeMillis();executionTime = task.nextExecutionTime;if (taskFired = (executionTime<=currentTime)) {if (task.period == 0) {// 一次性任務,刪除掉queue.removeMin();task.state = TimerTask.EXECUTED;} else {// 重復執行的任務,再提交一次queue.rescheduleMin(task.period<0 ? currentTime   - task.period: executionTime + task.period);}}}if (!taskFired)// 任務執行時間還沒到,繼續waitqueue.wait(executionTime - currentTime);}if (taskFired)// 任務執行時間到了,就執行task.run();} catch(InterruptedException e) {}}
}

注意,period>0 代表任務以 **固定速率 **執行,period<0 代表任務以 **固定延時 **執行。

什么意思呢?固定速率模式下,任務的下次執行時間會以上次任務的計劃執行時間開始計算,上次任務執行的耗時盡量不影響下次任務的執行時間。固定延時模式下,任務的下次執行時間會以上次任務的實際執行時間開始計算,也就是說上次任務的執行耗時會影響下次任務的執行時間。

TaskQueue

存儲任務的隊列被封裝成java.util.TaskQueue類,它是一個基于最小堆實現的優先隊列,堆中的元素會按照任務的計劃執行時間升序排列,隊頭永遠是最早要執行的任務,這樣獲取要執行的任務時間復雜度是O(1),但是任務的插入刪除,時間復雜度是O(logn)。

因為堆是一棵完全二叉樹,數據規模為n的情況下,二叉樹的高度是 logn。堆節點的插入和刪除,需要不斷和父節點或子節點比較并交換,交換的次數最多是樹的高度,所以時間復雜度最壞是O(logn)。

class TaskQueue {private TimerTask[] queue = new TimerTask[128];private int size = 0;
}

屬性說明:

  • queue 任務隊列,最小堆結構,用數組存儲
  • size 任務數

提交任務會調用add(),一個基本的往堆中添加節點的操作。先判斷數組是否要擴容,然后添加到隊尾,最后節點上濾,調整堆結構。

void add(TimerTask task) {// 擴容if (size + 1 == queue.length)queue = Arrays.copyOf(queue, 2*queue.length);// 先入到隊尾queue[++size] = task;// 上濾操作,調整堆結構fixUp(size);
}

堆節點上濾過程很簡單,不斷與父節點比較任務的執行時間,如果父節點任務晚于自己執行,就要和父節點交換位置。

/**
* 元素上濾,調整堆結構
* 不斷與父節點比較,如果父節點比自己大,就要和父節點交換
*/
private void fixUp(int k) {// 是否有父節點while (k > 1) {// j = 父節點下標int j = k >> 1;// 如果父節點的執行時間大于自己,就要交換,直到自己排到正確的位置if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)break;TimerTask tmp = queue[j];  queue[j] = queue[k]; queue[k] = tmp;k = j;}
}

如果是一次性任務,執行前要把它從堆中移除,調用removeMin()。先把隊尾節點賦值給隊頭節點,然后將隊頭節點下濾操作。

void removeMin() {queue[1] = queue[size];queue[size--] = null;fixDown(1);
}

堆節點下濾也很簡單,就是不斷和自己的左右子節點比較,如果子節點比自己小,就要交換,直到自己被交換到正確的位置。

private void fixDown(int k) {int j;// 是否有子節點 j=左子節點,j+1=右子節點while ((j = k << 1) <= size && j > 0) {if (j < size &&queue[j].nextExecutionTime > queue[j+1].nextExecutionTime)j++; // j indexes smallest kidif (queue[k].nextExecutionTime <= queue[j].nextExecutionTime)break;TimerTask tmp = queue[j];  queue[j] = queue[k]; queue[k] = tmp;k = j;}
}

尾巴

延時任務調度執行器的核心有兩點,分別是如何存儲任務以及如何調度執行任務。Java Timer 基于最小堆的數據結構來存儲延時任務,根節點永遠是最早執行的任務,獲取任務的時間復雜度是O(1),但是任務的插入和刪除需要O(logn)復雜度,這在需要維護大量延時任務時,會有性能問題,可以考慮采用時間輪算法。

另外,每個 Java Timer 對象都會開啟一個線程來調度執行提交的所有任務,因為是單線程執行,所以一旦有耗時的任務,隊列中的其它任務都會受到影響,這點尤其要注意。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/898449.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/898449.shtml
英文地址,請注明出處:http://en.pswp.cn/news/898449.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【安全運營】用戶與實體行為分析(UEBA)淺析

目錄 用戶與實體行為分析&#xff08;UEBA&#xff09;簡介一、UEBA的核心概念1. 行為基線建立2. 異常檢測3. 風險評分4. 上下文關聯 二、UEBA的應用場景1. 內部威脅檢測2. 外部威脅應對3. 合規性和審計支持 三、UEBA的技術實現1. 大數據技術2. 機器學習算法3. 可視化工具 四、…

系統思考—啤酒游戲經營決策沙盤模擬

再次感謝文華學院的邀請&#xff0c;為經緯集團管理層帶來 《啤酒游戲經營決策沙盤》&#xff01; 很多朋友問&#xff1a;“最近是不是啤酒游戲上的少了&#xff1f;” 其實&#xff0c;真正的關鍵不是游戲本身&#xff0c;而是——如何讓大家真正看見復雜系統中的隱性結構。 …

排序算法實現:插入排序與希爾排序

目錄 一、引言 二、代碼整體結構 三、宏定義與頭文件 四、插入排序函數&#xff08;Insertsort&#xff09; 函數作用 代碼要點分析 五、希爾排序函數&#xff08;ShellSort&#xff09; 函數作用 代碼要點分析 六、打印數組函數&#xff08;PrintSort&#x…

redis的key是如何找到對應存儲的數據原理

在 Redis 中,Key 是數據的唯一標識符,而 Value 是與 Key 關聯的實際數據。Redis 通過高效的鍵值對存儲機制,能夠快速定位和訪問數據。以下是 Redis 如何通過 Key 找到對應存儲數據的詳細解析: 1. Redis 的數據存儲結構 Redis 是一個基于內存的鍵值存儲系統,其核心數據結構…

github上傳本地文件到遠程倉庫(空倉庫/已有文件的倉庫)

今天搞自己本地訓練的代碼到倉庫留個檔&#xff0c;結果遇到了好多問題&#xff0c;到騰了半天才搞明白整個過程&#xff0c;留在這里記錄一下。 遠程空倉庫 主要根據官方教程&#xff1a;Adding locally hosted code to GitHub - GitHub Docs #1. cd到你需要上傳的文件夾&a…

Redis數據類型詳解

Redis數據類型詳解 Redis 共有 5 種基本數據類型&#xff1a;String&#xff08;字符串&#xff09;、List&#xff08;列表&#xff09;、Set&#xff08;集合&#xff09;、Hash&#xff08;散列&#xff09;、Zset&#xff08;有序集合&#xff09;。 除了 5 種基本的數據…

【第13章】億級電商平臺訂單系統-高性能之異步架構設計

1-1 本章導學 課程導學 學習目標:掌握大型系統架構設計難點之高性能異步架構設計項目落地:訂單系統高性能異步架構設計(年交易200億B2B電商平臺)本章主要內容 1. 為何需要異步消息架構 分析同步架構的性能瓶頸異步架構對系統解耦與性能提升的核心價值2. 確定異步消息技術…

2025-03-20 學習記錄--C/C++-C 庫函數 - toupper()、tolower()、 isspace()

合抱之木&#xff0c;生于毫末&#xff1b;九層之臺&#xff0c;起于累土&#xff1b;千里之行&#xff0c;始于足下。&#x1f4aa;&#x1f3fb; 一、C 庫函數 - toupper() ?? C 標準庫 - <ctype.h> C 標準庫的 ctype.h 頭文件提供了一些函數&#xff0c;可用于測試和…

LoRaWAN技術解析

LoRaWAN&#xff08;Long Range Wide Area Network&#xff09;是一種基于 LoRa&#xff08;Long Range&#xff09;技術的低功耗廣域網絡協議&#xff0c;專為物聯網&#xff08;IoT&#xff09;設備的無線通信而設計。它是一種開放的、標準化的通信協議&#xff0c;支持大規模…

織夢DedeCMS如何獲得在列表和文章頁獲得頂級或上級欄目名稱

獲得頂級或二級欄目的名稱&#xff0c;都需要修改php文件&#xff0c;修改的文件【/include/common.func.php】將代碼插入到這個文件的最下面即可&#xff1b; 一、獲得當前文章或欄目的【頂級欄目】名稱 1、插入頂級欄目代段 //獲取頂級欄目名 function GetTopTypename($id…

虛幻基礎:ue自定義類

文章目錄 Gameplay Tag&#xff1a;ue標簽類創建&#xff1a;其他-數據表格-gameplaytag安裝&#xff1a;項目設置&#xff1a;gamePlayTag&#xff1a;gamePlay標簽列表使用&#xff1a;變量類型&#xff1a;gamePlayTag primary data asset&#xff1a;ue數據類&#xff1a;通…

易語言模擬真人鼠標軌跡算法

一.簡介 鼠標軌跡算法是一種模擬人類鼠標操作的程序&#xff0c;它能夠模擬出自然而真實的鼠標移動路徑。 鼠標軌跡算法的底層實現采用C/C語言&#xff0c;原因在于C/C提供了高性能的執行能力和直接訪問操作系統底層資源的能力。 鼠標軌跡算法具有以下優勢&#xff1a; 模擬…

Matplotlib 柱形圖

Matplotlib 柱形圖 引言 在數據可視化領域&#xff0c;柱形圖是一種非常常見且強大的圖表類型。它能夠幫助我們直觀地比較不同類別或組之間的數據大小。Matplotlib&#xff0c;作為Python中最受歡迎的數據可視化庫之一&#xff0c;提供了豐富的繪圖功能&#xff0c;其中包括創…

sparksql的Transformation與 Action操作

Transformation操作 與RDD類似的操作 map、filter、flatMap、mapPartitions、sample、 randomSplit、 limit、 distinct、dropDuplicates、describe&#xff0c;而以上這些都是企業中比較常用的&#xff0c;這里在一個文件中統一論述 val df1 spark.read.json("src/m…

微軟Data Formulator:用AI重塑數據可視化的未來

在數據驅動的時代,如何快速將復雜數據轉化為直觀的圖表是每個分析師面臨的挑戰。微軟研究院推出的開源工具 Data Formulator,通過結合AI與交互式界面,重新定義了數據可視化的工作流。本文將深入解析這一工具的核心功能、安裝方法及使用技巧,助你輕松駕馭數據之美。 一、Dat…

20分鐘上手DeepSeek開發:SpringBoot + Vue2快速構建AI對話系統

20分鐘上手DeepSeek開發&#xff1a;SpringBoot Vue2快速構建AI對話系統 前言 在生成式AI技術蓬勃發展的今天&#xff0c;大語言模型已成為企業智能化轉型和個人效率提升的核心驅動力。作為國產大模型的優秀代表&#xff0c;DeepSeek憑借其卓越的中文語義理解能力和開發者友…

神經網絡中層與層之間的關聯

目錄 1. 層與層之間的核心關聯&#xff1a;數據流動與參數傳遞 1.1 數據流動&#xff08;Forward Propagation&#xff09; 1.2 參數傳遞&#xff08;Backward Propagation&#xff09; 2. 常見層與層之間的關聯模式 2.1 典型全連接網絡&#xff08;如手寫數字分類&#xf…

本地部署deepseek-r1建立向量知識庫和知識庫檢索實踐【代碼】

目錄 一、本地部署DS 二、建立本地知識庫 1.安裝python和必要的庫 2.設置主目錄工作區 3.編寫文檔解析腳本 4.構建向量數據庫 三、基于DS,使用本地知識庫檢索 本地部署DS,其實非常簡單,我寫了一篇操作記錄,我終于本地部署了DeepSeek-R1(圖文全過程)-CSDN博客 安裝…

String、StringBuffer、StringBuiler的區別

可變性 String是不可變的&#xff0c;這是因為String內部用于存儲數據的char[]數組用了final關鍵字修飾&#xff0c;而且是private的&#xff0c;并且沒有對外提供修改數組的方法。 StringBuffer和StringBuilder是可變的&#xff0c;它們內部的char數組沒有用final關鍵字修飾。…

Certd自動化申請和部署SSL證書并配置https

服務器使用的華為云&#xff0c;之前SSL證書通過配置Cloudflare的DNS實現的&#xff0c;最近華為云備案提示需修改解析至境內華為云IP&#xff0c;若解析境外IP&#xff0c;域名無需備案&#xff0c;需注銷或取消接入備案信息&#xff0c;改為使用Certd自搭建證書管理工具&…