本篇介紹Java併發編程基礎,內容皆總結摘抄自《Java併發編程的藝術》,僅作筆記。
線程簡介
現代操作系統在運行一個程序時,會爲其創建一個進程。現代操作系統調度的最小單元是線程,也叫輕量級進程,在一個進程裏可以創建多個線程,這些線程都擁有各自的計數器、堆棧和局部變量等屬性,並且能夠訪問共享的內存變量。處理器在這些線程上高速切換,讓使用者感覺到這些線程在同時執行。
一個Java程序從main()方法開始執行,然後按照既定的代碼邏輯執行,看似沒有其他線程參與,但實際上Java程序天生就是多線程程序,因爲執行main()方法的是一個名爲main的線程。
正確使用多線程,總是能夠給開發人員帶來顯著的好出,使用多線程的原因主要有以下幾點:
- 更多的處理器核心。線程是大多數操作系統調度的基本單元,一個程序作爲一個進程來運行,程序運行過程中能創建多個線程,而一個線程在一個時刻只能運行在一個處理器核心上。這樣一來,加入再多的處理器核心也無法顯著提高改程序的執行效率。而如果該程序使用多線程技術,將計算邏輯分配到多個處理器核心上,就會顯著減少程序的處理時間,隨着更多處理器核心的加入而變得更有效率。
- 更快的響應時間。對於一些有複雜業務邏輯的場景,可以使用多線程,將數據一致性不強的操作派發給其他線程處理。好處是響應用戶請求的線程能儘可能快的處理完成,縮短了響應時間,提升了用戶體驗。
- 更好的編程模型。Java爲多線程編程提供了良好、考究並且一致的編程模型,爲所遇到的問題建立合適的模型,而不是絞盡腦汁的考慮如何將其多線程化。
線程優先級
現代操作系統基本採用時分的形式調度運行的線程,操作系統會分出一個個時間片,線程會分配到若干時間片,當線程的時間片用完了就會發生線程調度,並等待着下次分配。線程分配到的時間片多少決定了線程使用處理器資源的多少,而線程優先級就是決定線程需要多或少分配一些處理器資源的線程屬性。
在Java線程中,通過一個整型成員變量priority來控制優先級,優先級的範圍從1~10,在線程構建的時候可以通過setPriority(int)方法來修改優先級,默認優先級是5,優先級高的線程分配時間片的數量要多與優先級低的線程。設置線程優先級時,針對頻繁阻塞(休眠或I/O操作)的線程需要設置較高優先級,而偏重計算(需要較多CPU時間或偏運算)的線程則設置較低的優先級,確保處理器不會被獨佔。在不同的JVM或操作系統上,線程規劃會存在差異,有些操作系統甚至會忽略對線程優先級的設定。
線程的狀態
Java線程在運行的生命週期中可能處於下表中的6種不同的狀態,在給定的一個時刻,線程只能處於其中的一個狀態。
狀態名稱 | 說明 |
---|---|
new | 初始狀態,線程被構建,但還沒有調用start()方法 |
runnable | 運行狀態,Java線程將操作系統中的就緒和運行兩種狀態籠統的稱爲運行中 |
blocked | 阻塞狀態,表示線程阻塞於鎖 |
waiting | 等待狀態,表示線程進入等待狀態,進入該狀態表示當前線程需要等待其他線程做出一些特定動作(通知或中斷) |
time_waiting | 超時等待狀態,該狀態不同於waiting,它是可以在指定的時間自行返回的 |
terminated | 終止狀態,表示當前線程已經執行完畢 |
線程在自身的生命週期中,並不是固定的出於某個狀態,而是隨着代碼的執行在不同的狀態之間進行切換。Java線程狀態變遷如下圖:
線程創建後,調用start()方法進入就緒狀態,當線程獲取到CPU時開始執行,進入運行中狀態。線程執行wait()方法進入等待狀態,進入等待狀態的線程需要依靠其他線程的通知才能返回到就緒狀態。而超時等待狀態相當於在等待狀態的基礎上增加了超時限制,也就是超時時間達到時將會返回到就緒狀態。當線程調用同步方法時,在沒有獲取到鎖的情況下,線程會進入到阻塞狀態,在線程獲取到同步鎖時線程進入就緒狀態。線程在執行Runnable的run()方法之後或執行過程中出現異常之後進入到終止狀態。
Daemon線程
Daemon時一種支持型線程,因爲它主要被用作程序中後臺調度以及支持性工作。即當一個Java虛擬機不存在非Daemon線程時,Java虛擬機將會退出。可以通過調用Thread.setDaemon(true)將線程設置爲Daemon線程。
Runnable和Thread API
在介紹線程實際應用前我們先來了解一下線程相關的API,此處先介紹Runnable以及Thread,其他的會在另外的文章中另作介紹。
Runnable
Runnable接口只有一個run()方法,實現此接口的類在run()方法中實現自己的線程執行體。
public abstract void run();
Thread
Thread類代表線程對象,用於操作線程,實現了Runnable接口。Thread類的常用構造函數如下:
//傳入Runnable接口的實現類對象
Thread(Runnable target);
//傳入Runnable接口的實現類對象並設置線程名稱
Thread(Runnable target, String name);
常用方法如下:
//獲得正在執行的線程對象的引用
public static native Thread currentThread();
//獲得線程id
public long getId();
//獲得線程名稱
public final String getName();
//獲得優先級
public final int getPriority();
//獲得線程狀態
public Thread.State getState();
//中斷這個線程,並非立即中斷,只是設置一箇中斷標識
public void interrupt();
//查看線程是否中斷,並清除中斷標識
public static boolean interrupted();
//此線程是否存活
public final native boolean isAlive();
//是否是守護線程
public final boolean isDaemon();
//線程是否中斷,不清除中斷標識
public static boolean interrupted();
//等到調用join方法的線程執行完畢再回到當前線程執行
public final void join();
//等到調用join方法的線程執行指定時間後回到當前線程繼續執行
public final synchronized void join(long millis);
//設置守護線程
public final void setDaemon(boolean on);
//設置線程名稱
public final synchronized void setName(String name);
//設置線程優先級
public final void setPriority(int newPriority);
//當前線程休眠指定時間
public static native void sleep(long millis);
//線程開始執行,即調用run()方法
public synchronized void start();
//當前線程讓出CPU,回到就緒狀態與其他線程重新競爭
public static native void yield();
啓動和終止線程
構造線程
在運行線程之前首先要構造一個線程對象,我們可以選擇實現Runnable接口和繼承Thread類。實現Runnable接口構造線程對象代碼如下:
public class MultiThread {
private volatile static boolean flag = false;
public static void main(String[] args) {
//創建Runnable接口實現類對象
MyThread mt = new MyThread();
//創建Thread線程對象,將Runnable實現類作爲參數
Thread thread = new Thread(mt);
//啓動線程
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("買到票了...");
}
static class MyThread implements Runnable{
@Override
public void run() {
while (!flag) {
System.out.println("在排隊呀...");
}
}
}
}
就像以上代碼中寫的,使用實現Runnable接口也要創建Thread類,畢竟操作線程的方法都在Thread類裏。另外,啓動線程是調用start方法,如果直接調用run方法就只是調用了一個普通的方法,並沒有真正啓動另外一個線程。
使用繼承Thread類構造線程代碼如下:
public class MultiThread {
private volatile static boolean flag = false;
public static void main(String[] args) {
//創建Thread類子類對象
MyThread thread = new MyThread();
//設置當前線程名稱
thread.setName("排隊");
//獲取線程id
System.out.println("當前線程id:"+thread.getId());
//獲取線程狀態
System.out.println("當前線程狀態:"+thread.getState());
//是否是守護線程
System.out.println("當前線程是否是守護線程:"+thread.isDaemon());
//啓動線程
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("買到票了...");
}
static class MyThread extends Thread{
@Override
public void run() {
while (!flag) {
System.out.println("在排隊呀...");
}
}
}
}
繼承Thread類後就無需再創建Thread對象,子類可以使用Thread類中的方法來操作線程。上面獲取線程狀態的返回值是NEW,這與前面介紹線程狀態時說的相吻合。
理解中斷
中斷可以理解爲線程的一個標識位屬性,它表示一個運行中的線程是否被其他線程進行了中斷操作。其他線程通過調用該線程的interrupt()方法對其進行中斷操作。
線程通過檢查自身是否被中斷來響應,線程通過方法isInterrupted()方法來進行判斷是否被中斷,也可以調用靜態方法Thread.interrupted()方法對當前線程的中斷標識位進行復位。如果該線程已經出於終結狀態,即使該線程被中斷過,在調用該線程對象的isInterrupted()方法時仍然會返回false。
下面的例子中,使用interrupted()方法來判斷線程是否被其他線程進行了中斷操作並且清除了中斷標識,因此在main方法中再次調用interrupted()方法返回false。
public class InterruptThread {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
System.out.println(thread.interrupted());
System.out.println("買到票啦...");
}
}
class MyThread extends Thread{
@Override
public void run() {
while (true) {
if (this.interrupted()){
break;
}
System.out.println("在排隊哦...");
}
}
}
線程間的通信
線程開始運行,擁有自己的棧空間,就如同一個腳本一樣,按照既定的代碼一步一步的執行,直到終止。但每個運行中的線程,如果僅僅是孤立的運行,價值就很少,如果多個線程能相互配合完成工作,將會帶來巨大的價值。
volatile和synchronized關鍵字
Java支持多個線程同時訪問一個對象或者對象的成員變量,由於每個線程可以擁有這個變量的拷貝,所以程序在執行過程中,一個線程看到的變量不一定是最新的。
關鍵字volatile可以用來修飾成員變量,告知程序任何對變量的訪問均需要從共享內存中獲取,而對它的改變必須同步刷新會共享內存,它能保證所有線程對變量訪問的可見性。
關鍵字synchronized可以修飾方法或以同步塊的形式來使用,主要確保多個線程在同一時刻,只能有一個線程出於方法或同步塊中,它保證了線程對變量訪問的可見性和排他性。
任何一個對象都擁有自己的監視器,當這個對象由同步塊或者這個對象的同步方法調用時,執行方法的線程必須先獲取到該對象的監視器才能進入同步塊或同步方法,而沒有獲取到監視器的線程將會阻塞在同步塊或同步方法的入口處,進入阻塞狀態。
等待/通知機制
一個線程修改了一個對象的值,另一個線程感知到了變化,然後進行相應的操作,整個過程開始於一個線程,而最終執行又是另一個線程。前者是生產者,後者就是消費者,這種模式隔離了“做什麼”和“怎麼做”,在功能層面上實現瞭解耦,體系結構上具備了良好的伸縮性。
在Java語言中要實現類似的功能,簡單的方法時讓消費者線程不斷地循環檢查變量是否符合逾期,如下面代碼所示,在while循環中設置不滿足的條件,如果條件滿足則退出while循環,從而完成消費者的工作。
while(value != desire){
Thread.sleep(1000);
}
doSomething();
上面的僞代碼在條件不滿足時就睡眠一段時間,這樣做的目的是防止過快的無效嘗試,這種方式看似能實現所需的功能,但存在以下問題:
- 難以確保及時性。在睡眠時,基本不消耗處理器資源,但如果睡的過久,就不能及時發現條件已經變化。
- 難以降低開銷。如果降低睡眠時間,這樣消費者能更加迅速的發現條件變化,但可能消耗更多的處理器資源。
以上兩個問題可以通過Java內置的等待/通知機制解決。等待/通知的相關方法是任意Java對象都具備的,因爲這些方法被定義在所有對象的超類java.lang.Object上,方法和描述如下表。
方法名稱 | 描述 |
---|---|
notify() | 通知一個在對象上等待的線程,使其從wait()方法返回,而返回的前提是該線程獲取到了對象的鎖 |
notifyAll() | 通知所有等待在該對象上的線程 |
wait() | 調用該方法的線程進入waiting狀態,只有等待另外線程的通知或被中斷纔會返回,需要注意,調用wait()方法後,會釋放對象的鎖 |
wait(long) | 超時等待一段時間,參數時間是毫秒,即等到n毫秒,沒有通知就超時返回 |
wait(long,int) | 對於超時時間更細粒度的控制,可以達到納秒 |
等待/通知機制,是指一個線程A調用了對象O的wait()方法進入等待狀態,另一個線程B調用了對象O的notify()或者notifyAll()方法,線程A收到通知後從對象O的wait()方法返回,進而執行後續操作。上述兩個線程通過對象O完成交互,而對象上的wait()和notify()/notifyAll()的關係就如同開關信號一樣,用來完成等待方和通知方之間的交互工作。
在調用wait()、notify()以及notifyAll()時需要注意以下細節:
- 使用wait()、notify()以及notifyAll()時需要先對對象加鎖。
- 調用wait()方法後,線程狀態由RUNNING變爲WAITING,並將當前線程放置到對象的等待隊列。
- notify()或notifyAll()方法調用後,等待線程依舊不會從wait()返回,需要調用notify()或notifyAll()的線程釋放鎖後,等待線程纔有機會從wait()返回。
- notify()方法將等待隊列中的一個等待線程從等待隊列中移到同步隊列中,而notifyAll()方法則是將等待隊列的所有的線程全部移到同步隊列,被移動的線程狀態由WAITING變爲BLOCKED。
- 從wait()方法返回的前提是獲得了對象的鎖。
例如在如下代碼中創建了兩個線程Wait和Notify,前者檢查flag是否爲false,如果符合要求則進行後續操作,否則在lock上等待,後者在睡眠一段時間後對lock進行通知。
public class WaitAndNotify {
private static boolean flag = true;
private static Object lock = new Object();
public static void main(String[] args) {
Thread threadW = new Thread(new Wait());
Thread threadN = new Thread(new Notify());
threadW.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadN.start();
}
static class Wait implements Runnable{
@Override
public void run() {
//加鎖,擁有lock的Monitor
synchronized (lock){
System.out.println("等待線程的前半部分工作完成了...");
//條件不滿足時,繼續wait,同時釋放lock的鎖
while (flag) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//條件滿足時,完成工作
System.out.println("等待線程的後半部分也完成了...");
}
}
}
static class Notify implements Runnable{
@Override
public void run() {
//加鎖,獲取lock的Monitor
synchronized (lock){
//獲取到lock的鎖後,先通知,通知時不會釋放lock的鎖
//當前線程釋放了lock的鎖之後,等待線程才能從wait()方法返回
lock.notifyAll();
flag = false;
System.out.println("通知線程通知了...");
}
}
}
}
輸出結果如下:
等待線程的前半部分工作完成了...
通知線程通知了...
等待線程的後半部分也完成了...
Wait線程首先獲取了對象的鎖,然後調用了對象的wait()方法,放棄了鎖並進入了對象的等待隊列中,進入等待狀態。由於Wait線程釋放了對象的鎖,Notify線程隨後獲取了對象的鎖,並調用對象的notify()方法,將Wait線程從等待隊列移到同步隊列中,此時Wait線程的狀態變爲阻塞狀態。Notify線程釋放鎖之後,Wait再次獲取到鎖並從wait()方法返回繼續執行。
等待/通知的經典範式
從上面的例子中可以提煉出等待/通知的經典範式,該範式分爲兩部分,分別針對等待方(消費者)和通知方(生產者)。
等待方遵循如下原則:
- 獲取對象的鎖。
- 如果條件不滿足,調用對象的wait()方法,被通知後仍要檢查條件。
- 條件滿足則執行對應的邏輯。
通知方遵循如下規則:
- 獲得對象的鎖。
- 改變條件。
- 通知所有等待在對象上的線程。
ThreadLocal
ThreadLocal,即線程變量,是一個以ThreadLocal對象爲鍵、任意對象爲值的存儲結構。這個結構被附帶在線程上,即一個線程可以根據一個ThreadLocal對象查詢到綁定在這個線程上的一個值。也就是說,對於某個ThreadLocal變量,每個線程都有一個變量副本,並且每個線程操作的都是自己本地內存中的副本。
ThreadLocal的常用方法如下:
//返回當前線程的此線程局部變量的值
public T get();
//返回此局部變量在當前線程中的初始值
protected T initialValue();
//刪除此局部變量的當前線程的值
public void remove();
//將當前線程的此局部變量的值設置爲指定的值
public void set(T value);
例如下面的代碼,線程A和線程B分別在線程內部設置了ThreadLocal變量,在輸出時打印的也是與線程綁定的變量。
public class ThreadLocalTest {
private static ThreadLocal<String> threadLocalStr = new ThreadLocal<>();
public static void main(String[] args) {
MyThreadA threadA = new MyThreadA();
MyThreadB threadB = new MyThreadB();
threadA.start();
threadB.start();
}
static class MyThreadA extends Thread{
@Override
public void run() {
threadLocalStr.set("線程A");
printThread();
}
}
static class MyThreadB extends Thread{
@Override
public void run() {
threadLocalStr.set("線程B");
printThread();
}
}
private static void printThread(){
System.out.println(threadLocalStr.get());
}
}
輸出結果如下:
線程A
線程B