前言
這篇博客重點在於講解API功能細節,對於同步異步還沒有清晰概念,或從未使用過線程同步相關API的同學,請先自行補全入門基礎,這裏不再累述
由於Java爲線程API都設置了強制異常檢查,所以編程時需要編寫大量的try-catch代碼,爲了節省這些無意義代碼,讓博客更簡潔清晰,我們封裝了一個Threads工具類,來屏蔽這些異常檢查代碼。比如:
- Threads.post(runnable)相當於new Thread(runnable).start()
- Threads.sleep()相當於Thread.sleep()
- Threads.yield()相當於Thread.yield()
- Threads.wait(lock)相當於lock.wait()
- Threads.notify(lock)相當於lock.notify()
- Threads.join(thread)相當於thread.join()
- Threads.interrupt(thread)相當於thread.interrupt()
博客中提到的lock對象,均是指作爲同步鎖的對象,任意對象都可以作爲同步鎖
線程狀態
出於線程管理和描述的需要,我們必須清楚地定義出線程的所有狀態或生命週期
- 新建狀態(NEW):thread剛被new出來,還沒有執行start方法
- 就緒狀態(READY):thread已經在運行,但是尚未獲得CPU或同步鎖資源
- 運行狀態(RUNNABLE):thread獲得了CPU和同步鎖資源,正在執行代碼
- 阻塞狀態(BLOCKED):thread因爲sychronized,sleep,wait,join等原因,暫時停止執行(等待條件成立時,就會復原到就緒狀態)
- 中止狀態(TERMINATED):thread的run方法執行完畢,任務完成,線程結束
Thread.sleep(long ms)
讓當前線程休眠一段時間,達到指定時間後,再繼續執行後續代碼
synchronized(lock) {…}
同步塊,表示在{…}塊作用域內,當前線程獲得lock對象的訪問和修改權限,也可以形象地說成是,當前線程獲得lock對象鎖
在{…}塊結束之前,其它線程無法訪問或修改lock對象,需要一直等待
這樣說其實並不嚴謹,這只是最簡單的情況,通過Thread.wait等方法可以讓同步塊暫時讓出對線鎖,馬上就會提到
synchronized function() {…}
同步方法,同一時間只有一個線程可以進入被synchronized修飾的方法,只有當前線程退出方法後,其它線程才能進入,相當於是一個方法鎖
lock.wait()
只有在同步塊中,lock被作爲同步鎖時,才能使用此方法
這個方法讓當前線程臨時讓出lock的所有權,當前線程進入阻塞狀態
直到其它線程調用了lock.notify()或lock.notifyAll()通知資源已經釋放,纔會恢復到就緒狀態,重新競爭資源
代碼測試
//同步鎖
final Object lock = new Object();
//啓動線程A
Threads.post(() -> {
synchronized (lock) { //1.由於線程B休眠,所以線程A先進入同步塊,獲得lock所有權
Threads.sleep(5000); //2.線程A休眠,保持對lock的所有權
System.out.println("A1");
Threads.waits(lock); //4.線程A讓出lock所有權,等待其它線程notify,再重新競爭lock所有權
System.out.println("A2"); //由於兩個線程都沒有調用notify,所以A2和B2永遠不會打印,一直阻塞在wait處
}
});
//啓動線程B
Threads.post(() -> {
Threads.sleep(2000);
synchronized (lock) { //3.由於lock被線程A佔有,進入阻塞狀態,等待lock資源
System.out.println("B1"); //5.線程B獲得lock所有權
Threads.waits(lock); //6.線程B讓出lock所有權,等待其它線程notify,再重新競爭lock所有權
System.out.println("B2");
}
});
lock.wait(long ms)
同wait()功能一樣,但是增加了超時處理
當超出timeout指定的時間時,即使其它線程沒有調用lock.notify(),線程也會自動進入就緒狀態,重新競爭lock資源
代碼測試
//同步鎖
final Object lock = new Object();
//啓動線程A
Threads.post(() -> {
synchronized (lock) { //1.由於線程B休眠,所以線程A先進入同步塊,獲得lock所有權
Threads.sleep(5000); //2.線程A休眠,保持對lock的所有權
System.out.println("A1");
Threads.waits(lock, 5000); //4.線程A讓出lock所有權,等待其它線程notify或超時,再重新競爭lock所有權
System.out.println("A2"); //5.wait超時,線程自動切換到就緒狀態,獲得lock所有權
}
});
//啓動線程B
Threads.post(() -> {
Threads.sleep(2000);
synchronized (lock) { //3.由於lock被線程A佔有,進入阻塞狀態,等待lock資源
System.out.println("B1"); //5.線程B獲得lock所有權
Threads.waits(lock); //6.線程B讓出lock所有權,等待其它線程notify,再重新競爭lock所有權
System.out.println("B2"); //由於線程A沒有調用notify,所以B2永遠不會打印
}
});
sleep和wait方法的區別
- sleep是Thread類的方法,wait是Object類的方法
- sleep(休眠)是當前線程什麼都不做,和其它線程互不影響,沒有如何互動
- wait(等待)則是交出同步鎖所有權,並且等待其它線程的通知,wait是圍繞着同步鎖進行的線程間互動
lock.notify()
通知另一個正處於wait狀態的線程退出wait狀態,進入就緒狀態,開始競爭lock資源
如果有多個線程都處於wait狀態,具體哪個線程被通知是不確定的,由系統調度決定
還有一點非常重要的就是,線程調用notify,並不意味着就立刻釋放lock鎖,僅僅是通知其它線程而已,其它線程也只是開始競爭資源,並不代表可以立刻得到lock資源,只有notify的線程退出同步塊之後,lock資源纔會被釋放,其它線程纔有可能搶到資源
一般建議將notify放在同步塊的最後一行代碼執行,因爲就算提前執行也沒用,反而容易引起誤會或重複調用
lock.notifyAll()
和nofify方法功能一致,只不過nofify方法只通知一個線程,而notifyAll通知所有處於wait狀態的線程
代碼測試
//同步鎖
final Object lock = new Object();
//由於多個線程是併發運行的,沒法控制哪個先執行,這樣就不方便測試
//但是我們可以通過休眠,來控制有效代碼的執行順序
//我們讓三個線程分別休眠1s,2s,3s,這樣代碼執行順序就是A-B-C
//啓動線程A
Threads.post(() -> {
Threads.sleep(100);
synchronized (lock) {
System.out.println("A1"); //1.線程A獲得同步鎖,開始執行代碼
Threads.wait(lock); //2.線程A交出同步鎖,進入wait狀態
System.out.println("A2");
}
});
//啓動線程B
Threads.post(() -> {
Threads.sleep(200);
synchronized (lock) {
Threads.sleep(500); //3.線程B獲得同步鎖,開始執行代碼
System.out.println("B1");
Threads.wait(lock); //5.線程B交出同步鎖,進入wait狀態
System.out.println("B2");
}
});
//啓動線程C
Threads.post(() -> {
Threads.sleep(300);
synchronized (lock) { //4.由於同步鎖被B佔有,無法進入同步塊
System.out.println("C1"); //6.A和B都交出了同步鎖,線程C進入同步塊,獲得同步鎖
Threads.notify(lock); //7.通知其中一個wait線程退出阻塞狀態,進入就緒狀態
Threads.sleep(500); //8.由於線程C仍持有同步鎖,wait線程只能繼續等待,雖然它已經是就緒狀態
System.out.println("C2");
} //9.線程C釋放同步鎖,wait線程獲得同步鎖,繼續執行代碼,但由於只通知了一個線程,A2和B2只有一個會打印
});
Thread.yield()
它告訴系統,可以讓當前線程先放棄lock資源,從運行狀態轉入就緒狀態,從而讓其它線程有獲得lock資源的機會
它是一個建議性的API,並不能保證其它線程一定能得到lock資源
因爲就緒狀態的線程仍然會競爭資源,可能剛剛讓出lock資源,馬上又給自己搶到了,但是這樣至少保證了其它線程有得到lock資源的可能性
一般在我們不想壟斷資源,也不想永遠在其它線程之後執行,想讓不同線程隨機自由競爭的時候,就可以使用這個API
由於它是建議性的,運行效果也是隨機的,一般我們並沒有必要調用這個方法,往往我們都是出於"完美主義"的想法,想"公平對待"不同線程,纔會去使用它
值得注意的是,Thread.sleep()和Thread.yield()都是靜態方法,屬於Thread類的方法,而不是屬於某個對象的方法,它們的操作對象都是當前線程
thread.setPriority(int priority)
提到了Thread.yield方法,就順便提下thread.setPriority方法,它也是一個建議性的方法
它爲線程設置不同的優先級,取值範圍爲1-10,數值越大優先級越高
高優先級線程有更高概率獲得CPU資源和鎖資源,但這不是一定生效的,只是給CPU的一個建議
thread.join()
讓另一個線程先執行,執行完再回到當前線程繼續執行,這個thread一般是其它線程
這個方法不涉及同步鎖,僅僅是控制多個線程的執行順序,但也會讓線程進入阻塞狀態
thread.join(long ms)
和thread.join()功能一致,但是增加了超時限制,達到指定時間,即使其它線程未執行完畢,也會繼續執行
代碼測試
//同步鎖
final Object lock = new Object();
//創建線程B
Thread t2 = new Thread(() -> {
Threads.sleep(3000); //3.線程B優先執行,線程A等待
System.out.println("B1");
System.out.println("B2");
}); //4.線程B執行完畢
//創建線程A
Thread t1 = new Thread(() -> {
System.out.println("A1"); //1.由於線程B在休眠,所以A先執行
Threads.join(t2); //2.等待B執行完畢,再繼續執行
System.out.println("A2"); //5.線程A繼續執行
});
//啓動線程
t1.start();
t2.start();
線程阻塞的幾種情景
- 由於sychronized無法獲得對線鎖,進入阻塞狀態,其它線程讓出同步鎖時,即可打破阻塞狀態
- 由於sleep方法,主動進入阻塞狀態,達到超時時間,即可退出阻塞狀態
- 由於wait方法,主動進入阻塞狀態,收到其它線程的notify,或達到超時時間,即可打破阻塞狀態
- 由於join方法,主動進入阻塞狀態,其它線程執行完畢,或達到超時時間,即可打破阻塞狀態
thread.interrupt()
中斷一個處於阻塞狀態的線程,並拋出一個InterruptedException
注意兩點,一是隻能中斷處於阻塞狀態的線程,不能中斷處於正常運行狀態的線程,二是中斷後只是拋出一個異常,並不是直接讓線程停止
如果我們正確處理了這個異常,線程還是會繼續往下執行,當然,我們也可以在捕獲到異常時,通過代碼讓線程return或跳到最後一行,從而達到停止線程的目的
注意,interrupt()方法只對sleep,wait,join方法引起的阻塞狀態有效,對sychronized同步鎖造成的阻塞無效
代碼測試
//同步鎖
final Object lock = new Object();
//創建線程C
Thread t3 = new Thread(() -> {
synchronized (lock) {
Threads.sleep(100000);
}
});
//創建線程A
Thread t1 = new Thread(() -> {
System.out.println("A1"); //1.線程A和C先運行,因爲線程B在休眠
try {
Threads.join(t3); //2.等待線程C運行完畢,由於C睡眠時間很長,A會長時間阻塞
} catch (Exception e) { //6.線程A被打斷,拋出InterruptedException異常
System.out.println("InterruptedException");
return; //7.捕獲異常,return結束線程,也可以不結束,取決於代碼
}
System.out.println("A2"); //8.由於線程結束,A2不會被打印
});
//創建線程B
Thread t2 = new Thread(() -> {
Threads.sleep(1000);
System.out.println("B1"); //3.線程B開始運行
t1.interrupt(); //4.打斷線程A的阻塞狀態
System.out.println("B2"); //5.線程B繼續執行
});
//啓動線程
t1.start();
t2.start();
t3.start();
thread.stop()
強制無條件立刻終止一個線程
這個方法比interrupt更加暴力,它可以在任何情景下立刻終止一個線程,不管線程run方法是否執行完,不管線程是否處於阻塞狀態,或線程是否擁有同步鎖,它都會立刻生效
這是個已經被廢棄的方法,因爲它的結束方式就決定了,這個接口與生俱來的危險性
比如這個線程使用了同時向一個List和一個Map對象寫數據,並給讀寫操作加鎖了,結果這個線程給List寫入了數據,剛準備給Map寫數據的時候,線程就被強制結束了,當其它線程再使用List和Map的時候,使用的已經是不同步的數據了
但是如果確定線程操作不涉及線程同步或內存泄露等問題,使用stop方法還是不錯的選擇的,畢竟它是唯一一個讓線程立即結束的方法,非常簡單
代碼測試
//創建線程A
Thread t1 = new Thread(() -> {
while (true) {
long millis = System.currentTimeMillis();
System.out.println(millis);
}
});
//創建線程B
Thread t2 = new Thread(() -> {
Thread.sleeps(2000);
t1.stop();
});
//啓動線程
t1.start();
t2.start();
Java源碼中對Thread.State的定義
我們在文章的剛開始,就已經講解過線程狀態的定義,但是我們是從線程工作原理的角度來講的,它適用於所有語言
Java在Thread也定義了一個名爲State的內部類,可以通過thread.getState()來獲取線程的State,Thread源碼中定義的線程狀態則和我們上面定義的有所差異
我們先來看下Java源碼
package java.lang;
public class Thread implements Runnable {
public enum State {
NEW, BLOCKED, WAITING, TIMED_WAITING, TERMINATED;
}
private volatile int threadStatus = 0;
public State getState() {
return sun.misc.VM.toThreadState(threadStatus);
}
}
通過源碼我們可以看到以下區別
- Java源碼中沒有定義就緒狀態,因爲就緒狀態只存在一瞬間,如果競爭資源成功,馬上就會進入運行狀態,如果失敗,則會立刻轉入阻塞狀態,從代碼實現的角度來說,一瞬間的狀態是沒實際意義的,因爲它的值馬上就會發生變化
- Java源碼中將阻塞狀態分爲了三種:BLOCKED, WAITING, TIMED_WAITING
- sychronized引起的同步鎖阻塞,用BLOCKED表示
- wait(),join()引起的無限等待阻塞,用WAITING表示
- wait(ms),join(ms),sleep(ms)引起的限時等待阻塞,用TIMED_WAITING表示
- 可以看到,和我們在文章的劃分其實本質上是一樣的,只是表述上的區別。因爲源碼是爲了實現Thread接口功能所設計的,它必須區分每種具體的狀態,才能實現wait,notify這些功能
Java內存模型和volatile關鍵字
Java內存模型我們在這裏不詳解,只是爲了介紹volatile而簡單提下
在Java的內存模型中,爲了保證速度,每個CPU都有一個高速緩存,線程使用變量時,首先訪問的並不是內存中的值,而是CPU緩存中的值。這樣就會出現一個問題,當多個線程使用不同CPU的時候,是從不同的CPU緩存中取值,這樣就有可能會出現變量值不一致的情況
volatile關鍵字能夠保證被其修飾的變量,在數值被改變時,能及時地反映到內存和各個CPU緩存中,這樣就能每個線程獲取到的值是最新的
volatile只能保證取值賦值語句在多線程情況下可以正確執行,並不能保證其它情景下變量值也是同步的,甚至是非常簡單的語句都不行
volatile的作用非常有限,我們看下這個例子就知道了
private volatile int sum = 0;
//這就是我們要舉例的語句,非常之簡單
//但是volatile關鍵字不能保證這條語句能夠正確地執行
sum++;
//volatile只能保證下面這種形式的語句能夠按照預期的結果執行
sum = 100;
int result = sum;
//sum++其實相當於以下語句
sum = sum + 1;
//再進一步,它其實相當於這樣的語句
int temp = sum;
sum = temp + 1;
//這就是問題的關鍵所在
//volatile可以保證int temp = sum的正確性
//volatile也可以保證sum = temp + 1的正確性
//但是這是對CPU來說,是兩次運算,在兩次運算之間,其它線程可能已經修改了sum的值
//比如我們有10000個線程都在這些sum++這個簡單的代碼
//我們的線程第一個拿到了sum的值,它的初始值爲0,於是
//int temp = 0
//sum = temp + 1 = 1;
//但是在這兩句之間,其它的線程可能已經讓sum自增了100次,sum的最新值已經變成了100
//而我們卻還在用舊的sum值在做自增運算,得到1,然後賦值給sum,覆蓋了其它線程的運算結果
//這顯然不是我們所預期的結果
//問題的關鍵就在於,sum++是複合操作,它其實相當於多個運算語句
//而多個運算語句之間,其它線程是有可能插入進來先執行的,讓我們的操作變得無意義
//所以正如前面所說的,volatile只能保證基本取值賦值語句的正確性
//從這個例子我們可以看出,volatile的功能其實極其有限
//不知道大家有沒有悟出來一個結論:
//volatile其實並不是用來解決線程間的語句同步問題的,而是用來解決CPU之間的變量值同步問題
有了sychronized還需要volatile嗎
看來上節的說明,不知道大家有沒有自己悟出來這樣一點:
volatile其實並不是用來解決線程間的語句同步問題的,而是用來解決CPU之間的變量值同步問題
sychronized關鍵字用於解決線程間的語句同步問題,它將同步塊作爲一個整體,其它線程只有在整個同步塊都退出時,纔有可能修改或訪問同步變量
除此之外,其實sychronized關鍵字也具有保證CPU之間變量值同步的功能,sychronized在進入同步塊時,會清空所有的CPU緩存,從主內存中重新獲得變量值,sychronized在退出同步塊時,會將最新的變量值同步到主內存當中
雖然sychronized的功能要強大於volatile,但是由於sychronized是阻塞式的,它會影響到代碼的執行速度,一個線程在執行,其它線程就要等待。而且sychronized本身在實現上,就比volatile更加複雜,運行效率更低
所以在一些簡單的情況下,比如一個線程只寫值,另一個線程只讀值,也不用關心多線程下語句的執行順序時,使用volatile就足夠了
代碼測試
我們通過代碼來測試下,沒有volatile關鍵字,會不會出現變量值不同步的問題
@SuppressWarnings("all")
public class Hello {
public static Integer value = 0;
public static void main(String[] args) {
//線程A先執行,value=0
Threads.post(() -> {
while (true)
if (value != 0)
System.out.println("Value Change");
//永遠不會打印,說明線程B的修改沒有反映到線程A中
//如果我們在value前加上volatile修飾,則馬上打印,說明volatile確實具有同步數值的作用
//另外,即使我們不使用volatile關鍵字
//如果我們在while (true)裏面添加sychronized或sleep語句,發現也會打印語句
//這說明,進入sychronized同步塊或執行sleep語句後,會自動同步數值
//注意:這個測試代碼其實是有講究的,我們不能直接通過打印value的值去測試
//因爲System.out.println方法本身內部就包含了sychronized代碼在裏面
//如果直接打印,必然會造成變量同步,這樣是測不出真實結果的
});
//線程B後執行,修改value的值
Threads.post(() -> {
Threads.sleep(200);
while (true)
value = 999;
});
}
}
工具類代碼
補上工具類代碼,方便大家做伸手黨
@SuppressWarnings("all")
public class Threads {
public static void post(Runnable runnable) {
new Thread(runnable).start();
}
public static void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void wait(Object lock) {
try {
lock.wait();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void wait(Object lock, long ms) {
try {
lock.wait(ms);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void notify(Object lock) {
try {
lock.notify();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void notifyAll(Object lock) {
try {
lock.notifyAll();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void yield() {
try {
Thread.yield();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void join(Thread thread) {
try {
thread.join();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void join(Thread thread, long ms) {
try {
thread.join(ms);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void interrupt(Thread thread) {
try {
thread.interrupt();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}