本文部分摘自《Java 並發編程的藝術》
線程簡介
1. 什么是線程?
現代操作系統在運行一個程序時,會為其創建一個進程,一個進程里可以創建多個線程。現代操作系統調度的最小單元是線程,也叫輕量級進程。這些線程都擁有各自的計數器、堆棧和局部變量等屬性,並且能訪問共享的內存變量。處理器在這些線程上高速切換,讓使用者覺得這些線程在同時執行
2. 為什么使用多線程?
使用多線程的原因主要有以下幾點:
-
更多的處理器核心
通過使用多線程技術,將計算邏輯分配到多個處理器核心上,可以顯著減少程序的處理時間
-
更快的響應時間
有時我們會編寫一些較為復雜的代碼(主要指業務邏輯),可以使用多線程技術,將數據一致性不強的操作派發給其他線程處理(也可以使用消息隊列)。這樣做的好處是響應用戶請求的線程能夠盡可能快地處理完成,縮短了響應時間
-
更好的編程模型
Java 已經為多線程編程提供了一套良好的編程模型,開發人員只需根據問題需要建立合適的模型即可
線程優先級
現代操作系統基本采用時分的形式調度運行的線程,操作系統會分出一個個時間片,線程分配到若干時間片,當線程的時間片用完了發生線程調度,並等待下次分配。線程分配到的時間片多少也就決定了線程使用處理器資源的多少,而線程優先級就是決定線程需要多或少分配一些處理器資源的線程屬性
在 Java 線程中,通過一個整型成員變量 priority 來控制優先級,優先級的范圍從 1 ~ 10,在線程構建時可以通過 setPriority(int) 方法來修改優先級,默認優先級是 5,優先級高的線程分配時間片的數量要多於優先級低的線程。不過,在不同的 JVM 以及操作系統上,線程規划會存在差異,有些操作系統甚至會忽略線程優先級的設定
public class Priority {
private static volatile boolean notStart = true;
private static volatile boolean notEnd = true;
public static void main(String[] args) throws Exception {
List<Job> jobs = new ArrayList<Job>();
for (int i = 0; i < 10; i++) {
int priority = i < 5 ? Thread.MIN_PRIORITY : Thread.MAX_PRIORITY;
Job job = new Job(priority);
jobs.add(job);
Thread thread = new Thread(job, "Thread:" + i);
thread.setPriority(priority);
thread.start();
}
notStart = false;
TimeUnit.SECONDS.sleep(10);
notEnd = false;
for (Job job : jobs) {
System.out.println("Job Priority : " + job.priority + ", Count : " + job.jobCount);
}
}
static class Job implements Runnable {
private int priority;
private long jobCount;
public Job(int priority) {
this.priority = priority;
}
@Override
public void run() {
while (notStart) {
Thread.yield();
}
while (notEnd) {
Thread.yield();
jobCount++;
}
}
}
}
運行該示例,在筆者機器上對應的輸出如下
筆者使用的環境為:Win10 + JDK11,從輸出可以看到線程優先級起作用了
線程的狀態
Java 線程在運行的生命周期中可能處於下表所示的六種不同的狀態,在給定的一個時刻,線程只能處於其中的一個狀態
狀態名稱 | 說明 |
---|---|
NEW | 初始狀態,線程被構建,但還沒調用 start() 方法 |
RUNNABLE | 運行狀態,Java 線程將操作系統中的就緒和運行兩種狀態籠統地稱作“運行中” |
BLOCKED | 阻塞狀態,表示線程阻塞於鎖 |
WAITING | 等待狀態,表示線程進入等待狀態,進入該狀態表示當前線程需要等待其他線程做出一些特定動作(通知或中斷) |
TIME_WAITING | 超時等待狀態,該狀態不同於 WAITING,它是可以在指定的時間自行返回的 |
TERMINATED | 終止狀態,表示當前線程已經執行完畢 |
線程在自身的生命周期中,並不是固定地處於某一狀態,而是隨着代碼的執行在不同的狀態之間進行切換
Daemon 線程
Daemon 線程是一種支持型線程,主要被用作程序中后台調度以及支持性工作。這意味着,當一個 Java 虛擬機中不存在 Daemon 線程的時候,Java 虛擬機將退出。可以調用 Thread.setDaemon(true) 將線程設置為 Daemon 線程
使用 Daemon 線程需要注意兩點:
- Daemon 屬性需要在啟動線程之前設置,不能在啟動線程之后設置
- 在構建 Daemon 線程時,不能依靠 finally 塊中的內容來確保執行或關閉清理資源的邏輯。因為在 Java 虛擬機退出時 Daemon 線程中的 finally 塊並不一定會執行
啟動和終止線程
1. 構造線程
在運行線程之前首先要構造一個線程對象,線程對象在構造的時候需提供線程需的屬性,如線程所屬的線程組、是否是 Daemon 線程等信息
2. 啟動線程
線程對象在初始化完成之后,調用 start() 方法即可啟動線程
3. 理解中斷
中斷可以理解為線程的一個標識位屬性,標識一個運行中的線程是否被其他線程進行了中斷操作。中斷好比其他線程對該線程打了個招呼,其他線程可以通過調用該線程的 interrupt() 方法對其進行中斷操作
線程通過檢查自身是否被中斷進行響應,線程通過 isInterrupted() 來進行判斷是否被中斷,也可以調用靜態方法 Tread.interrupted() 對當前線程的中斷標識位進行復位。如果線程已經處於終結狀態,即時線程被中斷過,在調用該對象的 isInterrupted() 時依舊會返回 false
許多聲明拋出 InterruptedException 的方法在拋出異常之前,Java 虛擬機會先將該線程的中斷標識位清除,然后拋出 InterruptedException,此時調用 isInterrupted() 方法將會返回 false
在下面的例子中,首先創建兩個線程 SleepThread 和 BusyThread,前者不停地睡眠,后者一直運行,分別對兩個線程分別進行中斷操作,觀察中斷標識位
public class Interrupted {
public static void main(String[] args) throws InterruptedException {
// sleepThread 不停的嘗試睡眠
Thread sleepThread = new Thread(new SleepRunner(), "SleepThread");
sleepThread.setDaemon(true);
// busyThread 不停的運行
Thread busyThread = new Thread(new BusyRunner(), "BusyThread");
busyThread.setDaemon(true);
sleepThread.start();
busyThread.start();
// 休眠 5 秒,讓 sleepThread 和 busyThread 充分運行
TimeUnit.SECONDS.sleep(5);
sleepThread.interrupt();
busyThread.interrupt();
System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted());
System.out.println("BusyThread interrupted is " + busyThread.isInterrupted());
// 防止 sleepThread 和 busyThreaad 立刻退出
SleepUtils.second(2);
}
static class SleepRunner implements Runnable {
@Override
public void run() {
while (true) {
SleepUtils.second(10);
}
}
}
static class BusyRunner implements Runnable {
@Override
public void run() {
while (true) {
}
}
}
}
輸出如下
從結果可以看出,拋出 InterruptedException 的線程 SleepThread,其中斷標識位被清除了,而一直忙碌運行的線程 BusyThread 的中斷標識位沒有被清除
4. 安全地終止線程
前面提到的中斷操作是一種簡便的線程間交互方式,適合用來取消或停止任務。除了中斷以外,還可以利用一個 boolean 變量來控制是否需要停止任務並終止線程
下面的示例中,創建了一個線程 CountThread,它不斷地進行變量累加,而主線程嘗試對其進行中斷操作和停止操作
public class Shutdown {
public static void main(String[] args) throws InterruptedException {
Runner one = new Runner();
Thread countThread = new Thread(one, "CountThread");
countThread.start();
// 睡眠一秒,main 線程對 CountThread 進行中斷,使 CountThread 能夠感知中斷而結束
TimeUnit.SECONDS.sleep(1);
countThread.interrupt();
Runner two = new Runner();
countThread = new Thread(two, "CountThread");
countThread.start();
// 睡眠一秒,main 線程對 Runner two 進行中斷,使 CountThread 能夠感知 on 為 false 而結束
TimeUnit.SECONDS.sleep(1);
two.cancel();
}
private static class Runner implements Runnable {
private long i;
private volatile boolean on = true;
@Override
public void run() {
while (on && !Thread.currentThread().isInterrupted()) {
i++;
}
System.out.println("Count i = " + i);
}
public void cancel() {
on = false;
}
}
}
main 線程通過中斷操作和 cancel() 方法均可使 CountThread 得以終止。這種通過標識位或者中斷操作的方式能夠使線程在終止時有機會去清理資源,而不是武斷地將線程停止,更加安全和優雅