本文部分摘自《Java 并發(fā)編程的藝術(shù)》
現(xiàn)代操作系統(tǒng)在運行一個程序時,會為其創(chuàng)建一個進程,一個進程里可以創(chuàng)建多個線程?,F(xiàn)代操作系統(tǒng)調(diào)度的最小單元是線程,也叫輕量級進程。這些線程都擁有各自的計數(shù)器、堆棧和局部變量等屬性,并且能訪問共享的內(nèi)存變量。處理器在這些線程上高速切換,讓使用者覺得這些線程在同時執(zhí)行
使用多線程的原因主要有以下幾點:
更多的處理器核心
通過使用多線程技術(shù),將計算邏輯分配到多個處理器核心上,可以顯著減少程序的處理時間
更快的響應時間
有時我們會編寫一些較為復雜的代碼(主要指業(yè)務邏輯),可以使用多線程技術(shù),將數(shù)據(jù)一致性不強的操作派發(fā)給其他線程處理(也可以使用消息隊列)。這樣做的好處是響應用戶請求的線程能夠盡可能快地處理完成,縮短了響應時間
更好的編程模型
Java 已經(jīng)為多線程編程提供了一套良好的編程模型,開發(fā)人員只需根據(jù)問題需要建立合適的模型即可
現(xiàn)代操作系統(tǒng)基本采用時分的形式調(diào)度運行的線程,操作系統(tǒng)會分出一個個時間片,線程分配到若干時間片,當線程的時間片用完了發(fā)生線程調(diào)度,并等待下次分配。線程分配到的時間片多少也就決定了線程使用處理器資源的多少,而線程優(yōu)先級就是決定線程需要多或少分配一些處理器資源的線程屬性
在 Java 線程中,通過一個整型成員變量 priority 來控制優(yōu)先級,優(yōu)先級的范圍從 1 ~ 10,在線程構(gòu)建時可以通過 setPriority(int) 方法來修改優(yōu)先級,默認優(yōu)先級是 5,優(yōu)先級高的線程分配時間片的數(shù)量要多于優(yōu)先級低的線程。不過,在不同的 JVM 以及操作系統(tǒng)上,線程規(guī)劃會存在差異,有些操作系統(tǒng)甚至會忽略線程優(yōu)先級的設(shè)定
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++;
}
}
}
}
運行該示例,在筆者機器上對應的輸出如下
筆者使用的環(huán)境為:Win10 + JDK11,從輸出可以看到線程優(yōu)先級起作用了
Java 線程在運行的生命周期中可能處于下表所示的六種不同的狀態(tài),在給定的一個時刻,線程只能處于其中的一個狀態(tài)
狀態(tài)名稱 | 說明 |
---|---|
NEW | 初始狀態(tài),線程被構(gòu)建,但還沒調(diào)用 start() 方法 |
RUNNABLE | 運行狀態(tài),Java 線程將操作系統(tǒng)中的就緒和運行兩種狀態(tài)籠統(tǒng)地稱作“運行中” |
BLOCKED | 阻塞狀態(tài),表示線程阻塞于鎖 |
WAITING | 等待狀態(tài),表示線程進入等待狀態(tài),進入該狀態(tài)表示當前線程需要等待其他線程做出一些特定動作(通知或中斷) |
TIME_WAITING | 超時等待狀態(tài),該狀態(tài)不同于 WAITING,它是可以在指定的時間自行返回的 |
TERMINATED | 終止狀態(tài),表示當前線程已經(jīng)執(zhí)行完畢 |
線程在自身的生命周期中,并不是固定地處于某一狀態(tài),而是隨著代碼的執(zhí)行在不同的狀態(tài)之間進行切換
Daemon 線程是一種支持型線程,主要被用作程序中后臺調(diào)度以及支持性工作。這意味著,當一個 Java 虛擬機中不存在 Daemon 線程的時候,Java 虛擬機將退出??梢哉{(diào)用 Thread.setDaemon(true) 將線程設(shè)置為 Daemon 線程
使用 Daemon 線程需要注意兩點:
在運行線程之前首先要構(gòu)造一個線程對象,線程對象在構(gòu)造的時候需提供線程需的屬性,如線程所屬的線程組、是否是 Daemon 線程等信息
線程對象在初始化完成之后,調(diào)用 start() 方法即可啟動線程
中斷可以理解為線程的一個標識位屬性,標識一個運行中的線程是否被其他線程進行了中斷操作。中斷好比其他線程對該線程打了個招呼,其他線程可以通過調(diào)用該線程的 interrupt() 方法對其進行中斷操作
線程通過檢查自身是否被中斷進行響應,線程通過 isInterrupted() 來進行判斷是否被中斷,也可以調(diào)用靜態(tài)方法 Tread.interrupted() 對當前線程的中斷標識位進行復位。如果線程已經(jīng)處于終結(jié)狀態(tài),即時線程被中斷過,在調(diào)用該對象的 isInterrupted() 時依舊會返回 false
許多聲明拋出 InterruptedException 的方法在拋出異常之前,Java 虛擬機會先將該線程的中斷標識位清除,然后拋出 InterruptedException,此時調(diào)用 isInterrupted() 方法將會返回 false
在下面的例子中,首先創(chuàng)建兩個線程 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) {
}
}
}
}
輸出如下
從結(jié)果可以看出,拋出 InterruptedException 的線程 SleepThread,其中斷標識位被清除了,而一直忙碌運行的線程 BusyThread 的中斷標識位沒有被清除
前面提到的中斷操作是一種簡便的線程間交互方式,適合用來取消或停止任務。除了中斷以外,還可以利用一個 boolean 變量來控制是否需要停止任務并終止線程
下面的示例中,創(chuàng)建了一個線程 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 能夠感知中斷而結(jié)束
TimeUnit.SECONDS.sleep(1);
countThread.interrupt();
Runner two = new Runner();
countThread = new Thread(two, "CountThread");
countThread.start();
// 睡眠一秒,main 線程對 Runner two 進行中斷,使 CountThread 能夠感知 on 為 false 而結(jié)束
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 得以終止。這種通過標識位或者中斷操作的方式能夠使線程在終止時有機會去清理資源,而不是武斷地將線程停止,更加安全和優(yōu)雅