每次要延遲執行時,很多人只想到 Thread.sleep()。但你知道嗎?有七種寫法可以選,每種各有優缺點。選對的話,程式真香,選錯的話,維護時會想揍自己。

方法 1:Thread.sleep(ms) - 最基本

沒啥好說的,最原始的方式:

1
2
3
4
5
6
7
try {
System.out.println("Before sleep");
Thread.sleep(2000); // 睡 2 秒
System.out.println("After sleep");
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 重新設定中斷狀態
}

優點: 簡單直接
缺點: 毫秒單位,容易打錯;InterruptedException 必須處理;不易讀

方法 2:TimeUnit.SECONDS.sleep(n) - 可讀性最好

用 TimeUnit 包裝,讓意圖更清楚:

1
2
3
4
5
6
7
try {
System.out.println("Before sleep");
TimeUnit.SECONDS.sleep(2); // 清楚地表示 2 秒
System.out.println("After sleep");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

TimeUnit 有多種單位:

1
2
3
4
TimeUnit.MILLISECONDS.sleep(500);   // 500 毫秒
TimeUnit.SECONDS.sleep(2); // 2 秒
TimeUnit.MINUTES.sleep(1); // 1 分鐘
TimeUnit.HOURS.sleep(1); // 1 小時

優點: 可讀性超好,單位清楚,不易出錯
缺點: 還是要 catch InterruptedException

我的建議:一般專案就用這個,真香。

方法 3:ScheduledExecutorService - 用排程執行

適合定期或延遲執行的任務:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

Runnable task = () -> System.out.println("Task executed");

// 延遲 2 秒後執行一次
ScheduledFuture<?> future = scheduler.schedule(task, 2, TimeUnit.SECONDS);

// 等待完成
try {
future.get(); // 會阻塞直到任務完成
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

scheduler.shutdown();

定期執行(每 2 秒重複一次):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

// 延遲 1 秒開始,每 2 秒執行一次
scheduler.scheduleAtFixedRate(
() -> System.out.println("Repeating task"),
1, // 初始延遲
2, // 週期
TimeUnit.SECONDS
);

// 程式繼續跑,scheduler 在背景執行

// 優雅關閉(等待現有任務完成,但不接受新任務)
scheduler.shutdown();
scheduler.awaitTermination(10, TimeUnit.SECONDS);

優點: 功能強大,支援定期任務、Future 取值
缺點: 複雜度高,要管理 executor service 的生命週期

方法 4:CompletableFuture.delayedExecutor - Java 9+

用 CompletableFuture 的 delayedExecutor:

1
2
3
4
5
6
7
8
CompletableFuture<Void> future = CompletableFuture
.runAsync(
() -> System.out.println("Task executed"),
CompletableFuture.delayedExecutor(2, TimeUnit.SECONDS)
);

// 等待完成
future.join(); // 比 get() 更乾淨,不拋 checked exception

或結合其他 CompletableFuture 操作:

1
2
3
4
5
6
7
8
CompletableFuture
.supplyAsync(() -> {
System.out.println("Getting data");
return "result";
})
.delayedExecutor(2, TimeUnit.SECONDS)
.thenAccept(result -> System.out.println("Received: " + result))
.join();

優點: 函式式風格,能鏈接其他操作
缺點: 需要 Java 9+,使用 delayedExecutor() 的語法有點彆扭

方法 5:Timer/TimerTask - 舊式但仍可用

這是 Java 1.3 時代的遺跡,但在舊專案還是會看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Timer timer = new Timer();

timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Task executed");
}
}, 2000); // 2000 毫秒後執行

// 定期執行
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println("Repeating task");
}
}, 1000, 2000); // 延遲 1 秒,然後每 2 秒重複

// 記得關閉
timer.cancel();

優點: 很多舊程式都在用,改不動就只能用這個
缺點: 已過時,Timer 是單執行緒的(有瓶頸),ScheduledExecutorService 是它的升級版

盡量別用,除非維護舊程式。

方法 6:LockSupport.parkNanos - 底層 API

這是 JDK 內部用的,效率高但難用:

1
2
3
System.out.println("Before park");
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(2));
System.out.println("After park");

或毫秒版本:

1
2
3
// parkNanos 期望的是納秒(nanosecond)
long nanos = TimeUnit.MILLISECONDS.toNanos(2000);
LockSupport.parkNanos(nanos);

何時用? 通常你用不到,除非寫高性能的併發框架(如 Disruptor)。

優點: 效率最高,允許被中斷
缺點: 低層 API,難理解,容易用錯

方法 7:Object.wait(timeout) - 配合 synchronized

用 wait 搭配 notify/notifyAll,適合執行緒間通信:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class WaitExample {
private Object lock = new Object();

public void waitForNotification() throws InterruptedException {
synchronized(lock) {
System.out.println("Waiting...");
lock.wait(2000); // 等 2 秒或直到被 notify
System.out.println("Wait complete");
}
}

public void sendNotification() {
synchronized(lock) {
lock.notifyAll(); // 喚醒所有等待的執行緒
}
}
}

// 使用
WaitExample example = new WaitExample();

Thread t1 = new Thread(() -> {
try {
example.waitForNotification();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});

t1.start();

Thread.sleep(1000);
example.sendNotification(); // 1 秒後喚醒 t1
t1.join();

優點: 能於時間內被提前喚醒(非被動等待)
缺點: 複雜,容易踩坑(虛假喚醒 spurious wakeup)

七種方法的對比表

方法 單位清晰 易讀 功能強 複雜度 推薦度
Thread.sleep() ⭐⭐
TimeUnit.sleep() ⭐⭐⭐⭐⭐
ScheduledExecutorService ⭐⭐⭐⭐
CompletableFuture.delayedExecutor() ⭐⭐⭐
Timer/TimerTask
LockSupport.parkNanos() ⭐⭐
Object.wait() ⭐⭐⭐

實務建議

場景 1:簡單延遲(一次性)

1
2
// 最佳方案
TimeUnit.SECONDS.sleep(2);

場景 2:定期任務

1
2
3
// 最佳方案
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(task, 0, 2, TimeUnit.SECONDS);

場景 3:執行緒間通信 + 超時

1
2
3
4
// 最佳方案
synchronized(lock) {
lock.wait(2000);
}

場景 4:複雜的非同步流程(Java 9+)

1
2
// 最佳方案
CompletableFuture.delayedExecutor(2, TimeUnit.SECONDS)

場景 5:維護舊專案

1
// 迫不得已才用 Timer,改成 ScheduledExecutorService 更好

常見踩坑

坑 1:InterruptedException 別吞掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 錯誤 - 吞掉 exception,執行緒中斷狀態丟失
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
// 什麼都不做 ❌
}

// 正確 - 重新設定中斷狀態或拋出
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // ✓
// 或拋出 exception ✓
}

坑 2:Timer 的單執行緒限制

1
2
3
4
5
Timer timer = new Timer(); // 只有一個執行緒!

// 如果一個任務耗時很久,後面的任務會被延遲
timer.schedule(longRunningTask, 0);
timer.schedule(quickTask, 100); // 可能等待 longRunningTask 完成

用 ScheduledExecutorService 的執行緒池代替。

坑 3:wait() 的虛假喚醒

1
2
3
4
5
6
7
8
9
10
11
12
13
// 錯誤 - if 檢查一次就夠
synchronized(lock) {
if (condition) {
lock.wait();
}
}

// 正確 - 要迴圈檢查,因為可能虛假喚醒
synchronized(lock) {
while (!condition) {
lock.wait();
}
}

最後的話

Thread.sleep() 這麼簡單的東西,其實背後藏著這麼多選項。大部分情況下,用 TimeUnit.SECONDS.sleep() 就夠了,簡潔又清楚。需要排程或執行緒通信時才考慮其他方案。選對工具,程式會感謝你。