很多時候,我們需要定時任務實現一些諸如重新整理,心跳,保活等功能。這些定時任務往往邏輯很簡單,使用定時任務的框架(例如springboot @Scheduled)往往大材小用。
下面是一個定時任務的典型寫法,每隔30s傳送心跳
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (true) {
try {
//傳送心跳的業務程式碼
heartbeat();
Thread.sleep(1000L * 30);
} catch (Exception e) {
//print the error log
e.printStackTrace();
}
}
});
t.start();
}
如果你使用了IDEA或者其他的Java整合開發環境,你會發現編輯器會提示你Call to 'Thread.sleep()' in a loop, probably busy-waiting
點開提示資訊,發現這樣的寫法有可能會導致忙等待
和死鎖
忙等待 busy-waiting
佔用大量cpu資源,cpu利用率會達到99%,可能會完全吃掉一核cpu資源,導致其他業務甚至是宿主機的異常。
你可能會說,這樣的寫法怎麼會導致忙等待 busy-waiting
呢,我明明已經sleep()
了呀,心跳任務每隔30s才執行一次啊。
如果heartbeat()
丟擲了異常(空指標,程式碼錯誤,網路錯誤等),sleep()
語句就會跳過,進入了異常分支,休眠30s的目的無法達到,程式就會進入死迴圈,以瘋狂的速度執行heartbeat()
語句。更有甚者,如果你捕獲異常並列印日誌,日誌甚至能很快寫滿整個硬碟。
當然,你也可以將sleep語句提到前面來,先執行Thread.sleep()
,這樣,可以規避忙等待
風險。但是還有另外一個坑在等待著你。點開Thread.sleep()
檔案
Causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds, subject to the precision and accuracy of system timers and schedulers. The thread does not lose ownership of any monitors.
重點在這一句The thread does not lose ownership of any monitors.
monitor
就是Java的重量級鎖,平時我們使用的synchronized
關鍵字就是基於monitor
實現的。也就是說,執行緒在sleep的過程中並不會釋放所持有的鎖,這會導致嚴重的併發問題,甚至是死鎖。你可能又會說,我寫的程式碼裡沒有鎖沒有synchronized
關鍵字,我能不能放心使用呢?
答案是不能,你的程式碼裡沒鎖,不代表你依賴的程式碼裡沒鎖,不代表後續的維護者不會加鎖。這是一個技術債務,在絕大多數的情況下都不會出問題,但也許有一天會暴雷。
作為一個負責人的開發者,作為一個有著程式碼潔癖的人,作為一個無法忍受IDEA黃色提示的人,作為一個簡潔至上的人,作為一個不想濫用框架的人,我推薦使用jdk自帶的java.util.Timer
程式碼如下
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
try {
//傳送心跳的業務程式碼
heartbeat();
} catch (Exception e) {
//print the error log
e.printStackTrace();
}
}
}, 1000L * 30, 1000L * 30);
}
同樣實現的是30s間隔執行心跳操作,使用Timer
不會有上述我們說的忙等待
和死鎖
的風險。Timer
內部使用了一個執行緒,和我們單獨new Thread()
的效果是一樣的。值得一提的是,Timer
是基於wait(),notify()
機制實現的,與sleep()
相比,wait()
會釋放鎖(也就是monitor)。