作者 | 納達丶無忌
如果對什麼是線程、什麼是進程仍存有疑惑,請先 Google 之,因為這兩個概念不在本文的範圍之內。
用多線程只有一個目的,那就是更好的利用 CPU 的資源,因為所有的多線程代碼都可以用單線程來實現。說這個話其實只有一半對,因為反應“多角色”的程序代碼,最起碼每個角色要給他一個線程吧,否則連實際場景都無法模擬,當然也沒法說能用單線程來實現:比如最常見的“生產者,消費者模型”。
很多人都對其中的一些概念不夠明確,如同步、併發等等,讓我們先建立一個數據字典,以免產生誤會。
- 多線程:指的是這個程序(一個進程)運行時產生了不止一個線程
- 並行與併發:
- 並行:多個 CPU 實例或者多臺機器同時執行一段處理邏輯,是真正的同時。
- 併發:通過 CPU 調度算法,讓用戶看上去同時執行,實際上從 CPU 操作層面不是真正的同時。併發往往在場景中有公用的資源,那麼針對這個公用的資源往往產生瓶頸,我們會用 TPS 或者 QPS 來反應這個系統的處理能力。
作者 | 納達丶無忌
如果對什麼是線程、什麼是進程仍存有疑惑,請先 Google 之,因為這兩個概念不在本文的範圍之內。
用多線程只有一個目的,那就是更好的利用 CPU 的資源,因為所有的多線程代碼都可以用單線程來實現。說這個話其實只有一半對,因為反應“多角色”的程序代碼,最起碼每個角色要給他一個線程吧,否則連實際場景都無法模擬,當然也沒法說能用單線程來實現:比如最常見的“生產者,消費者模型”。
很多人都對其中的一些概念不夠明確,如同步、併發等等,讓我們先建立一個數據字典,以免產生誤會。
- 多線程:指的是這個程序(一個進程)運行時產生了不止一個線程
- 並行與併發:
- 並行:多個 CPU 實例或者多臺機器同時執行一段處理邏輯,是真正的同時。
- 併發:通過 CPU 調度算法,讓用戶看上去同時執行,實際上從 CPU 操作層面不是真正的同時。併發往往在場景中有公用的資源,那麼針對這個公用的資源往往產生瓶頸,我們會用 TPS 或者 QPS 來反應這個系統的處理能力。
併發與並行
- 線程安全:經常用來描繪一段代碼。指在併發的情況之下,該代碼經過多線程使用,線程的調度順序不影響任何結果。這個時候使用多線程,我們只需要關注系統的內存,CPU 是不是夠用即可。反過來,線程不安全就意味著線程的調度順序會影響最終結果,如不加事務的轉賬代碼:
void transferMoney(User from, User to, float amount){
to.setMoney(to.getBalance() + amount);
from.setMoney(from.getBalance() - amount);
}
- 同步:Java 中的同步指的是通過人為的控制和調度,保證共享資源的多線程訪問成為線程安全,來保證結果的準確。如上面的代碼簡單加入 @synchronized 關鍵字。在保證結果準確的同時,提高性能,才是優秀的程序。線程安全的優先級高於性能。
好了,讓我們開始吧。我準備分成幾部分來總結涉及到多線程的內容:
1. 紮好馬步:線程的狀態
2. 內功心法:每個對象都有的方法(機制)
3. 太祖長拳:基本線程類
4. 九陰真經:高級多線程控制類
紮好馬步:線程的狀態
先來兩張圖:
作者 | 納達丶無忌
如果對什麼是線程、什麼是進程仍存有疑惑,請先 Google 之,因為這兩個概念不在本文的範圍之內。
用多線程只有一個目的,那就是更好的利用 CPU 的資源,因為所有的多線程代碼都可以用單線程來實現。說這個話其實只有一半對,因為反應“多角色”的程序代碼,最起碼每個角色要給他一個線程吧,否則連實際場景都無法模擬,當然也沒法說能用單線程來實現:比如最常見的“生產者,消費者模型”。
很多人都對其中的一些概念不夠明確,如同步、併發等等,讓我們先建立一個數據字典,以免產生誤會。
- 多線程:指的是這個程序(一個進程)運行時產生了不止一個線程
- 並行與併發:
- 並行:多個 CPU 實例或者多臺機器同時執行一段處理邏輯,是真正的同時。
- 併發:通過 CPU 調度算法,讓用戶看上去同時執行,實際上從 CPU 操作層面不是真正的同時。併發往往在場景中有公用的資源,那麼針對這個公用的資源往往產生瓶頸,我們會用 TPS 或者 QPS 來反應這個系統的處理能力。
併發與並行
- 線程安全:經常用來描繪一段代碼。指在併發的情況之下,該代碼經過多線程使用,線程的調度順序不影響任何結果。這個時候使用多線程,我們只需要關注系統的內存,CPU 是不是夠用即可。反過來,線程不安全就意味著線程的調度順序會影響最終結果,如不加事務的轉賬代碼:
void transferMoney(User from, User to, float amount){
to.setMoney(to.getBalance() + amount);
from.setMoney(from.getBalance() - amount);
}
- 同步:Java 中的同步指的是通過人為的控制和調度,保證共享資源的多線程訪問成為線程安全,來保證結果的準確。如上面的代碼簡單加入 @synchronized 關鍵字。在保證結果準確的同時,提高性能,才是優秀的程序。線程安全的優先級高於性能。
好了,讓我們開始吧。我準備分成幾部分來總結涉及到多線程的內容:
1. 紮好馬步:線程的狀態
2. 內功心法:每個對象都有的方法(機制)
3. 太祖長拳:基本線程類
4. 九陰真經:高級多線程控制類
紮好馬步:線程的狀態
先來兩張圖:
線程狀態
作者 | 納達丶無忌
如果對什麼是線程、什麼是進程仍存有疑惑,請先 Google 之,因為這兩個概念不在本文的範圍之內。
用多線程只有一個目的,那就是更好的利用 CPU 的資源,因為所有的多線程代碼都可以用單線程來實現。說這個話其實只有一半對,因為反應“多角色”的程序代碼,最起碼每個角色要給他一個線程吧,否則連實際場景都無法模擬,當然也沒法說能用單線程來實現:比如最常見的“生產者,消費者模型”。
很多人都對其中的一些概念不夠明確,如同步、併發等等,讓我們先建立一個數據字典,以免產生誤會。
- 多線程:指的是這個程序(一個進程)運行時產生了不止一個線程
- 並行與併發:
- 並行:多個 CPU 實例或者多臺機器同時執行一段處理邏輯,是真正的同時。
- 併發:通過 CPU 調度算法,讓用戶看上去同時執行,實際上從 CPU 操作層面不是真正的同時。併發往往在場景中有公用的資源,那麼針對這個公用的資源往往產生瓶頸,我們會用 TPS 或者 QPS 來反應這個系統的處理能力。
併發與並行
- 線程安全:經常用來描繪一段代碼。指在併發的情況之下,該代碼經過多線程使用,線程的調度順序不影響任何結果。這個時候使用多線程,我們只需要關注系統的內存,CPU 是不是夠用即可。反過來,線程不安全就意味著線程的調度順序會影響最終結果,如不加事務的轉賬代碼:
void transferMoney(User from, User to, float amount){
to.setMoney(to.getBalance() + amount);
from.setMoney(from.getBalance() - amount);
}
- 同步:Java 中的同步指的是通過人為的控制和調度,保證共享資源的多線程訪問成為線程安全,來保證結果的準確。如上面的代碼簡單加入 @synchronized 關鍵字。在保證結果準確的同時,提高性能,才是優秀的程序。線程安全的優先級高於性能。
好了,讓我們開始吧。我準備分成幾部分來總結涉及到多線程的內容:
1. 紮好馬步:線程的狀態
2. 內功心法:每個對象都有的方法(機制)
3. 太祖長拳:基本線程類
4. 九陰真經:高級多線程控制類
紮好馬步:線程的狀態
先來兩張圖:
線程狀態
線程狀態轉換
各種狀態一目瞭然,值得一提的是 "Blocked" 和 "Waiting" 這兩個狀態的區別:
- 線程在 Running 的過程中可能會遇到阻塞 (Blocked) 情況
- 對 Running 狀態的線程加同步鎖 (Synchronized) 使其進入 (lock blocked pool),同步鎖被釋放進入可運行狀 (Runnable)。從 jdk 源碼註釋來看,blocked 指的是對 monitor 的等待(可以參考下文的圖)即該線程位於等待區。
- 線程在 Running 的過程中可能會遇到等待(Waiting)情況
- 線程可以主動調用 object.wait 或者 sleep,或者 join(join內部調用的是 sleep ,所以可看成 sleep 的一種)進入。從 jdk 源碼註釋來看,Waiting 是等待另一個線程完成某一個操作,如 join 等待另一個完成執行,object.wait() 等待object.notify() 方法執行。
Waiting 狀態和 Blocked 狀態有點費解,我個人的理解是:Blocked 其實也是一種 wait ,等待的是 monitor ,但是和Waiting 狀態不一樣,舉個例子,有三個線程進入了同步塊,其中兩個調用了 object.wait(),進入了 Waiting 狀態,這時第三個調用了 object.notifyAll() ,這時候前兩個線程就一個轉移到了 Runnable,一個轉移到了 Blocked。
從下文的 monitor 結構圖來區別:每個 Monitor 在某個時刻,只能被一個線程擁有,該線程就是 “Active Thread”,而其它線程都是 “Waiting Thread”,分別在兩個隊列 “ Entry Set” 和 “Wait Set” 裡面等候。在 “Entry Set” 中等待的線程狀態 Blocked,從 jstack 的dump 中來看是 “Waiting for monitor entry”,而在 “Wait Set” 中等待的線程狀態是 Waiting,表現在 jstack 的 dump 中是 “in Object.wait()”。
此外,在 runnable 狀態的線程是處於被調度的線程,此時的調度順序是不一定的。Thread 類中的 yield 方法可以讓一個 running 狀態的線程轉入 runnable。
內功心法:每個對象都有的方法(機制)
synchronized, wait, notify 是任何對象都具有的同步工具。讓我們先來了解他們
作者 | 納達丶無忌
如果對什麼是線程、什麼是進程仍存有疑惑,請先 Google 之,因為這兩個概念不在本文的範圍之內。
用多線程只有一個目的,那就是更好的利用 CPU 的資源,因為所有的多線程代碼都可以用單線程來實現。說這個話其實只有一半對,因為反應“多角色”的程序代碼,最起碼每個角色要給他一個線程吧,否則連實際場景都無法模擬,當然也沒法說能用單線程來實現:比如最常見的“生產者,消費者模型”。
很多人都對其中的一些概念不夠明確,如同步、併發等等,讓我們先建立一個數據字典,以免產生誤會。
- 多線程:指的是這個程序(一個進程)運行時產生了不止一個線程
- 並行與併發:
- 並行:多個 CPU 實例或者多臺機器同時執行一段處理邏輯,是真正的同時。
- 併發:通過 CPU 調度算法,讓用戶看上去同時執行,實際上從 CPU 操作層面不是真正的同時。併發往往在場景中有公用的資源,那麼針對這個公用的資源往往產生瓶頸,我們會用 TPS 或者 QPS 來反應這個系統的處理能力。
併發與並行
- 線程安全:經常用來描繪一段代碼。指在併發的情況之下,該代碼經過多線程使用,線程的調度順序不影響任何結果。這個時候使用多線程,我們只需要關注系統的內存,CPU 是不是夠用即可。反過來,線程不安全就意味著線程的調度順序會影響最終結果,如不加事務的轉賬代碼:
void transferMoney(User from, User to, float amount){
to.setMoney(to.getBalance() + amount);
from.setMoney(from.getBalance() - amount);
}
- 同步:Java 中的同步指的是通過人為的控制和調度,保證共享資源的多線程訪問成為線程安全,來保證結果的準確。如上面的代碼簡單加入 @synchronized 關鍵字。在保證結果準確的同時,提高性能,才是優秀的程序。線程安全的優先級高於性能。
好了,讓我們開始吧。我準備分成幾部分來總結涉及到多線程的內容:
1. 紮好馬步:線程的狀態
2. 內功心法:每個對象都有的方法(機制)
3. 太祖長拳:基本線程類
4. 九陰真經:高級多線程控制類
紮好馬步:線程的狀態
先來兩張圖:
線程狀態
線程狀態轉換
各種狀態一目瞭然,值得一提的是 "Blocked" 和 "Waiting" 這兩個狀態的區別:
- 線程在 Running 的過程中可能會遇到阻塞 (Blocked) 情況
- 對 Running 狀態的線程加同步鎖 (Synchronized) 使其進入 (lock blocked pool),同步鎖被釋放進入可運行狀 (Runnable)。從 jdk 源碼註釋來看,blocked 指的是對 monitor 的等待(可以參考下文的圖)即該線程位於等待區。
- 線程在 Running 的過程中可能會遇到等待(Waiting)情況
- 線程可以主動調用 object.wait 或者 sleep,或者 join(join內部調用的是 sleep ,所以可看成 sleep 的一種)進入。從 jdk 源碼註釋來看,Waiting 是等待另一個線程完成某一個操作,如 join 等待另一個完成執行,object.wait() 等待object.notify() 方法執行。
Waiting 狀態和 Blocked 狀態有點費解,我個人的理解是:Blocked 其實也是一種 wait ,等待的是 monitor ,但是和Waiting 狀態不一樣,舉個例子,有三個線程進入了同步塊,其中兩個調用了 object.wait(),進入了 Waiting 狀態,這時第三個調用了 object.notifyAll() ,這時候前兩個線程就一個轉移到了 Runnable,一個轉移到了 Blocked。
從下文的 monitor 結構圖來區別:每個 Monitor 在某個時刻,只能被一個線程擁有,該線程就是 “Active Thread”,而其它線程都是 “Waiting Thread”,分別在兩個隊列 “ Entry Set” 和 “Wait Set” 裡面等候。在 “Entry Set” 中等待的線程狀態 Blocked,從 jstack 的dump 中來看是 “Waiting for monitor entry”,而在 “Wait Set” 中等待的線程狀態是 Waiting,表現在 jstack 的 dump 中是 “in Object.wait()”。
此外,在 runnable 狀態的線程是處於被調度的線程,此時的調度順序是不一定的。Thread 類中的 yield 方法可以讓一個 running 狀態的線程轉入 runnable。
內功心法:每個對象都有的方法(機制)
synchronized, wait, notify 是任何對象都具有的同步工具。讓我們先來了解他們
monitor
他們是應用於同步問題的人工線程調度工具。講其本質,首先就要明確 monitor 的概念,Java 中的每個對象都有一個監視器,來監測併發代碼的重入。在非多線程編碼時該監視器不發揮作用,反之如果在 synchronized 範圍內,監視器發揮作用。
wait/notify 必須存在於 synchronized 塊中。並且,這三個關鍵字針對的是同一個監視器(某對象的監視器)。這意味著 wait之後,其他線程可以進入同步塊執行。
當某代碼並不持有監視器的使用權時(如圖中5的狀態,即脫離同步塊)去 wait 或 notify,會拋出java.lang.IllegalMonitorStateException。
也包括在 synchronized 塊中去調用另一個對象的 wait/notify,因為不同對象的監視器不同,同樣會拋出此異常。
再講用法:
- synchronized 單獨使用:
- 代碼塊:如下,在多線程環境下,synchronized 塊中的方法獲取了 lock 實例的 monitor,如果實例相同,那麼只有一個線程能執行該塊內容
public class Thread1 implements Runnable {
Object lock;
public void run() {
synchronized(lock){
..do something
}
}
}
- 直接用於方法:相當於上面代碼中用 lock 來鎖定的效果,實際獲取的是 Thread1 類的 monitor。更進一步,如果修飾的是 static 方法,則鎖定該類所有實例
public class Thread1 implements Runnable {
public synchronized void run() {
..do something
}
}
- synchronized, wait, notify 結合:典型場景生產者消費者問題
/**
* 生產者生產出來的產品交給店員
*/
public synchronized void produce()
{
if(this.product >= MAX_PRODUCT)
{
try
{
wait();
System.out.println("產品已滿,請稍候再生產");
}
catch(InterruptedException e)
{
e.printStackTrace () ;
}
return;
}
this.product++;
System.out.println("生產者生產第" + this.product + "個產品.");
notifyAll(); //通知等待區的消費者可以取出產品了
}
/**
* 消費者從店員取產品
*/
public synchronized void consume()
{
if(this.product <= MIN_PRODUCT)
{
try
{
wait();
System.out.println("缺貨,稍候再取");
}
catch (InterruptedException e)
{
e.printStackTrace();
}
return;
}
System.out.println("消費者取走了第" + this.product + "個產品.");
this.product--;
notifyAll(); //通知等待去的生產者可以生產產品了
}
volatile
多線程的內存模型:main memory(主存)、working memory(線程棧),在處理數據時,線程會把值從主存 load 到本地棧,完成操作後再 save 回去 (volatile 關鍵詞的作用:每次針對該變量的操作都激發一次 load and save) 。
作者 | 納達丶無忌
如果對什麼是線程、什麼是進程仍存有疑惑,請先 Google 之,因為這兩個概念不在本文的範圍之內。
用多線程只有一個目的,那就是更好的利用 CPU 的資源,因為所有的多線程代碼都可以用單線程來實現。說這個話其實只有一半對,因為反應“多角色”的程序代碼,最起碼每個角色要給他一個線程吧,否則連實際場景都無法模擬,當然也沒法說能用單線程來實現:比如最常見的“生產者,消費者模型”。
很多人都對其中的一些概念不夠明確,如同步、併發等等,讓我們先建立一個數據字典,以免產生誤會。
- 多線程:指的是這個程序(一個進程)運行時產生了不止一個線程
- 並行與併發:
- 並行:多個 CPU 實例或者多臺機器同時執行一段處理邏輯,是真正的同時。
- 併發:通過 CPU 調度算法,讓用戶看上去同時執行,實際上從 CPU 操作層面不是真正的同時。併發往往在場景中有公用的資源,那麼針對這個公用的資源往往產生瓶頸,我們會用 TPS 或者 QPS 來反應這個系統的處理能力。
併發與並行
- 線程安全:經常用來描繪一段代碼。指在併發的情況之下,該代碼經過多線程使用,線程的調度順序不影響任何結果。這個時候使用多線程,我們只需要關注系統的內存,CPU 是不是夠用即可。反過來,線程不安全就意味著線程的調度順序會影響最終結果,如不加事務的轉賬代碼:
void transferMoney(User from, User to, float amount){
to.setMoney(to.getBalance() + amount);
from.setMoney(from.getBalance() - amount);
}
- 同步:Java 中的同步指的是通過人為的控制和調度,保證共享資源的多線程訪問成為線程安全,來保證結果的準確。如上面的代碼簡單加入 @synchronized 關鍵字。在保證結果準確的同時,提高性能,才是優秀的程序。線程安全的優先級高於性能。
好了,讓我們開始吧。我準備分成幾部分來總結涉及到多線程的內容:
1. 紮好馬步:線程的狀態
2. 內功心法:每個對象都有的方法(機制)
3. 太祖長拳:基本線程類
4. 九陰真經:高級多線程控制類
紮好馬步:線程的狀態
先來兩張圖:
線程狀態
線程狀態轉換
各種狀態一目瞭然,值得一提的是 "Blocked" 和 "Waiting" 這兩個狀態的區別:
- 線程在 Running 的過程中可能會遇到阻塞 (Blocked) 情況
- 對 Running 狀態的線程加同步鎖 (Synchronized) 使其進入 (lock blocked pool),同步鎖被釋放進入可運行狀 (Runnable)。從 jdk 源碼註釋來看,blocked 指的是對 monitor 的等待(可以參考下文的圖)即該線程位於等待區。
- 線程在 Running 的過程中可能會遇到等待(Waiting)情況
- 線程可以主動調用 object.wait 或者 sleep,或者 join(join內部調用的是 sleep ,所以可看成 sleep 的一種)進入。從 jdk 源碼註釋來看,Waiting 是等待另一個線程完成某一個操作,如 join 等待另一個完成執行,object.wait() 等待object.notify() 方法執行。
Waiting 狀態和 Blocked 狀態有點費解,我個人的理解是:Blocked 其實也是一種 wait ,等待的是 monitor ,但是和Waiting 狀態不一樣,舉個例子,有三個線程進入了同步塊,其中兩個調用了 object.wait(),進入了 Waiting 狀態,這時第三個調用了 object.notifyAll() ,這時候前兩個線程就一個轉移到了 Runnable,一個轉移到了 Blocked。
從下文的 monitor 結構圖來區別:每個 Monitor 在某個時刻,只能被一個線程擁有,該線程就是 “Active Thread”,而其它線程都是 “Waiting Thread”,分別在兩個隊列 “ Entry Set” 和 “Wait Set” 裡面等候。在 “Entry Set” 中等待的線程狀態 Blocked,從 jstack 的dump 中來看是 “Waiting for monitor entry”,而在 “Wait Set” 中等待的線程狀態是 Waiting,表現在 jstack 的 dump 中是 “in Object.wait()”。
此外,在 runnable 狀態的線程是處於被調度的線程,此時的調度順序是不一定的。Thread 類中的 yield 方法可以讓一個 running 狀態的線程轉入 runnable。
內功心法:每個對象都有的方法(機制)
synchronized, wait, notify 是任何對象都具有的同步工具。讓我們先來了解他們
monitor
他們是應用於同步問題的人工線程調度工具。講其本質,首先就要明確 monitor 的概念,Java 中的每個對象都有一個監視器,來監測併發代碼的重入。在非多線程編碼時該監視器不發揮作用,反之如果在 synchronized 範圍內,監視器發揮作用。
wait/notify 必須存在於 synchronized 塊中。並且,這三個關鍵字針對的是同一個監視器(某對象的監視器)。這意味著 wait之後,其他線程可以進入同步塊執行。
當某代碼並不持有監視器的使用權時(如圖中5的狀態,即脫離同步塊)去 wait 或 notify,會拋出java.lang.IllegalMonitorStateException。
也包括在 synchronized 塊中去調用另一個對象的 wait/notify,因為不同對象的監視器不同,同樣會拋出此異常。
再講用法:
- synchronized 單獨使用:
- 代碼塊:如下,在多線程環境下,synchronized 塊中的方法獲取了 lock 實例的 monitor,如果實例相同,那麼只有一個線程能執行該塊內容
public class Thread1 implements Runnable {
Object lock;
public void run() {
synchronized(lock){
..do something
}
}
}
- 直接用於方法:相當於上面代碼中用 lock 來鎖定的效果,實際獲取的是 Thread1 類的 monitor。更進一步,如果修飾的是 static 方法,則鎖定該類所有實例
public class Thread1 implements Runnable {
public synchronized void run() {
..do something
}
}
- synchronized, wait, notify 結合:典型場景生產者消費者問題
/**
* 生產者生產出來的產品交給店員
*/
public synchronized void produce()
{
if(this.product >= MAX_PRODUCT)
{
try
{
wait();
System.out.println("產品已滿,請稍候再生產");
}
catch(InterruptedException e)
{
e.printStackTrace () ;
}
return;
}
this.product++;
System.out.println("生產者生產第" + this.product + "個產品.");
notifyAll(); //通知等待區的消費者可以取出產品了
}
/**
* 消費者從店員取產品
*/
public synchronized void consume()
{
if(this.product <= MIN_PRODUCT)
{
try
{
wait();
System.out.println("缺貨,稍候再取");
}
catch (InterruptedException e)
{
e.printStackTrace();
}
return;
}
System.out.println("消費者取走了第" + this.product + "個產品.");
this.product--;
notifyAll(); //通知等待去的生產者可以生產產品了
}
volatile
多線程的內存模型:main memory(主存)、working memory(線程棧),在處理數據時,線程會把值從主存 load 到本地棧,完成操作後再 save 回去 (volatile 關鍵詞的作用:每次針對該變量的操作都激發一次 load and save) 。
volatile
針對多線程使用的變量如果不是 volatile 或者 final 修飾的,很有可能產生不可預知的結果(另一個線程修改了這個值,但是之後在某線程看到的是修改之前的值)。其實道理上講同一實例的同一屬性本身只有一個副本。但是多線程是會緩存值的,本質上,volatile 就是不去緩存,直接取值。在線程安全的情況下加 volatile 會犧牲性能。
太祖長拳:基本線程類
基本線程類指的是 Thread 類,Runnable 接口,Callable 接口
Thread 類實現了 Runnable 接口,啟動一個線程的方法:
MyThread my = new MyThread();
my.start();
Thread類相關方法
//當前線程可轉讓 cpu 控制權,讓別的就緒狀態線程運行(切換)
public static Thread.yield()
//暫停一段時間
public static Thread.sleep()
//在一個線程中調用 other.join(),將等待other執行完後才繼續本線程。
public join()
//後兩個函數皆可以被打斷
public interrupte()
關於中斷:它並不像 stop 方法那樣會中斷一個正在運行的線程。線程會不時地檢測中斷標識位,以判斷線程是否應該被中斷(中斷標識值是否為 true )。終端只會影響到 wait 狀態、sleep 狀態和 join 狀態。被打斷的線程會拋出 InterruptedException。
Thread.interrupted() 檢查當前線程是否發生中斷,返回boolean
synchronized 在獲鎖的過程中是不能被中斷的。
中斷是一個狀態!interrupt()方法只是將這個狀態置為 true 而已。所以說正常運行的程序不去檢測狀態,就不會終止,而 wait 等阻塞方法會去檢查並拋出異常。如果在正常運行的程序中添加while(!Thread.interrupted()) ,則同樣可以在中斷後離開代碼體
Thread類最佳實踐:
寫的時候最好要設置線程名稱 Thread.name,並設置線程組 ThreadGroup,目的是方便管理。在出現問題的時候,打印線程棧 (jstack -pid) 一眼就可以看出是哪個線程出的問題,這個線程是幹什麼的。
如何獲取線程中的異常
作者 | 納達丶無忌
如果對什麼是線程、什麼是進程仍存有疑惑,請先 Google 之,因為這兩個概念不在本文的範圍之內。
用多線程只有一個目的,那就是更好的利用 CPU 的資源,因為所有的多線程代碼都可以用單線程來實現。說這個話其實只有一半對,因為反應“多角色”的程序代碼,最起碼每個角色要給他一個線程吧,否則連實際場景都無法模擬,當然也沒法說能用單線程來實現:比如最常見的“生產者,消費者模型”。
很多人都對其中的一些概念不夠明確,如同步、併發等等,讓我們先建立一個數據字典,以免產生誤會。
- 多線程:指的是這個程序(一個進程)運行時產生了不止一個線程
- 並行與併發:
- 並行:多個 CPU 實例或者多臺機器同時執行一段處理邏輯,是真正的同時。
- 併發:通過 CPU 調度算法,讓用戶看上去同時執行,實際上從 CPU 操作層面不是真正的同時。併發往往在場景中有公用的資源,那麼針對這個公用的資源往往產生瓶頸,我們會用 TPS 或者 QPS 來反應這個系統的處理能力。
併發與並行
- 線程安全:經常用來描繪一段代碼。指在併發的情況之下,該代碼經過多線程使用,線程的調度順序不影響任何結果。這個時候使用多線程,我們只需要關注系統的內存,CPU 是不是夠用即可。反過來,線程不安全就意味著線程的調度順序會影響最終結果,如不加事務的轉賬代碼:
void transferMoney(User from, User to, float amount){
to.setMoney(to.getBalance() + amount);
from.setMoney(from.getBalance() - amount);
}
- 同步:Java 中的同步指的是通過人為的控制和調度,保證共享資源的多線程訪問成為線程安全,來保證結果的準確。如上面的代碼簡單加入 @synchronized 關鍵字。在保證結果準確的同時,提高性能,才是優秀的程序。線程安全的優先級高於性能。
好了,讓我們開始吧。我準備分成幾部分來總結涉及到多線程的內容:
1. 紮好馬步:線程的狀態
2. 內功心法:每個對象都有的方法(機制)
3. 太祖長拳:基本線程類
4. 九陰真經:高級多線程控制類
紮好馬步:線程的狀態
先來兩張圖:
線程狀態
線程狀態轉換
各種狀態一目瞭然,值得一提的是 "Blocked" 和 "Waiting" 這兩個狀態的區別:
- 線程在 Running 的過程中可能會遇到阻塞 (Blocked) 情況
- 對 Running 狀態的線程加同步鎖 (Synchronized) 使其進入 (lock blocked pool),同步鎖被釋放進入可運行狀 (Runnable)。從 jdk 源碼註釋來看,blocked 指的是對 monitor 的等待(可以參考下文的圖)即該線程位於等待區。
- 線程在 Running 的過程中可能會遇到等待(Waiting)情況
- 線程可以主動調用 object.wait 或者 sleep,或者 join(join內部調用的是 sleep ,所以可看成 sleep 的一種)進入。從 jdk 源碼註釋來看,Waiting 是等待另一個線程完成某一個操作,如 join 等待另一個完成執行,object.wait() 等待object.notify() 方法執行。
Waiting 狀態和 Blocked 狀態有點費解,我個人的理解是:Blocked 其實也是一種 wait ,等待的是 monitor ,但是和Waiting 狀態不一樣,舉個例子,有三個線程進入了同步塊,其中兩個調用了 object.wait(),進入了 Waiting 狀態,這時第三個調用了 object.notifyAll() ,這時候前兩個線程就一個轉移到了 Runnable,一個轉移到了 Blocked。
從下文的 monitor 結構圖來區別:每個 Monitor 在某個時刻,只能被一個線程擁有,該線程就是 “Active Thread”,而其它線程都是 “Waiting Thread”,分別在兩個隊列 “ Entry Set” 和 “Wait Set” 裡面等候。在 “Entry Set” 中等待的線程狀態 Blocked,從 jstack 的dump 中來看是 “Waiting for monitor entry”,而在 “Wait Set” 中等待的線程狀態是 Waiting,表現在 jstack 的 dump 中是 “in Object.wait()”。
此外,在 runnable 狀態的線程是處於被調度的線程,此時的調度順序是不一定的。Thread 類中的 yield 方法可以讓一個 running 狀態的線程轉入 runnable。
內功心法:每個對象都有的方法(機制)
synchronized, wait, notify 是任何對象都具有的同步工具。讓我們先來了解他們
monitor
他們是應用於同步問題的人工線程調度工具。講其本質,首先就要明確 monitor 的概念,Java 中的每個對象都有一個監視器,來監測併發代碼的重入。在非多線程編碼時該監視器不發揮作用,反之如果在 synchronized 範圍內,監視器發揮作用。
wait/notify 必須存在於 synchronized 塊中。並且,這三個關鍵字針對的是同一個監視器(某對象的監視器)。這意味著 wait之後,其他線程可以進入同步塊執行。
當某代碼並不持有監視器的使用權時(如圖中5的狀態,即脫離同步塊)去 wait 或 notify,會拋出java.lang.IllegalMonitorStateException。
也包括在 synchronized 塊中去調用另一個對象的 wait/notify,因為不同對象的監視器不同,同樣會拋出此異常。
再講用法:
- synchronized 單獨使用:
- 代碼塊:如下,在多線程環境下,synchronized 塊中的方法獲取了 lock 實例的 monitor,如果實例相同,那麼只有一個線程能執行該塊內容
public class Thread1 implements Runnable {
Object lock;
public void run() {
synchronized(lock){
..do something
}
}
}
- 直接用於方法:相當於上面代碼中用 lock 來鎖定的效果,實際獲取的是 Thread1 類的 monitor。更進一步,如果修飾的是 static 方法,則鎖定該類所有實例
public class Thread1 implements Runnable {
public synchronized void run() {
..do something
}
}
- synchronized, wait, notify 結合:典型場景生產者消費者問題
/**
* 生產者生產出來的產品交給店員
*/
public synchronized void produce()
{
if(this.product >= MAX_PRODUCT)
{
try
{
wait();
System.out.println("產品已滿,請稍候再生產");
}
catch(InterruptedException e)
{
e.printStackTrace () ;
}
return;
}
this.product++;
System.out.println("生產者生產第" + this.product + "個產品.");
notifyAll(); //通知等待區的消費者可以取出產品了
}
/**
* 消費者從店員取產品
*/
public synchronized void consume()
{
if(this.product <= MIN_PRODUCT)
{
try
{
wait();
System.out.println("缺貨,稍候再取");
}
catch (InterruptedException e)
{
e.printStackTrace();
}
return;
}
System.out.println("消費者取走了第" + this.product + "個產品.");
this.product--;
notifyAll(); //通知等待去的生產者可以生產產品了
}
volatile
多線程的內存模型:main memory(主存)、working memory(線程棧),在處理數據時,線程會把值從主存 load 到本地棧,完成操作後再 save 回去 (volatile 關鍵詞的作用:每次針對該變量的操作都激發一次 load and save) 。
volatile
針對多線程使用的變量如果不是 volatile 或者 final 修飾的,很有可能產生不可預知的結果(另一個線程修改了這個值,但是之後在某線程看到的是修改之前的值)。其實道理上講同一實例的同一屬性本身只有一個副本。但是多線程是會緩存值的,本質上,volatile 就是不去緩存,直接取值。在線程安全的情況下加 volatile 會犧牲性能。
太祖長拳:基本線程類
基本線程類指的是 Thread 類,Runnable 接口,Callable 接口
Thread 類實現了 Runnable 接口,啟動一個線程的方法:
MyThread my = new MyThread();
my.start();
Thread類相關方法
//當前線程可轉讓 cpu 控制權,讓別的就緒狀態線程運行(切換)
public static Thread.yield()
//暫停一段時間
public static Thread.sleep()
//在一個線程中調用 other.join(),將等待other執行完後才繼續本線程。
public join()
//後兩個函數皆可以被打斷
public interrupte()
關於中斷:它並不像 stop 方法那樣會中斷一個正在運行的線程。線程會不時地檢測中斷標識位,以判斷線程是否應該被中斷(中斷標識值是否為 true )。終端只會影響到 wait 狀態、sleep 狀態和 join 狀態。被打斷的線程會拋出 InterruptedException。
Thread.interrupted() 檢查當前線程是否發生中斷,返回boolean
synchronized 在獲鎖的過程中是不能被中斷的。
中斷是一個狀態!interrupt()方法只是將這個狀態置為 true 而已。所以說正常運行的程序不去檢測狀態,就不會終止,而 wait 等阻塞方法會去檢查並拋出異常。如果在正常運行的程序中添加while(!Thread.interrupted()) ,則同樣可以在中斷後離開代碼體
Thread類最佳實踐:
寫的時候最好要設置線程名稱 Thread.name,並設置線程組 ThreadGroup,目的是方便管理。在出現問題的時候,打印線程棧 (jstack -pid) 一眼就可以看出是哪個線程出的問題,這個線程是幹什麼的。
如何獲取線程中的異常
不能用try,catch來獲取線程中的異常
Runnable
與 Thread 類似
Callable
future 模式:併發模式的一種,可以有兩種形式,即無阻塞和阻塞,分別是 isDone 和 get。其中 Future 對象用來存放該線程的返回值以及狀態
ExecutorService e = Executors.newFixedThreadPool(3);
//submit 方法有多重參數版本,及支持 callable 也能夠支持runnable 接口類型.
Future future = e.submit(new myCallable());
future.isDone() //return true,false 無阻塞
future.get() // return 返回值,阻塞直到該線程運行結束
九陰真經:高級多線程控制類
以上都屬於內功心法,接下來是實際項目中常用到的工具了,Java1.5 提供了一個非常高效實用的多線程包: java.util.concurrent, 提供了大量高級工具,可以幫助開發者編寫高效、易維護、結構清晰的 Java 多線程程序。
1.ThreadLocal類
用處:保存線程的獨立變量。對一個線程類(繼承自 Thread )
當使用 ThreadLocal 維護變量時,ThreadLocal 為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。常用於用戶登錄控制,如記錄 session 信息。
實現:每個Thread 都持有一個 TreadLocalMap 類型的變量(該類是一個輕量級的 Map,功能與 map 一樣,區別是桶裡放的是 entry 而不是 entry 的鏈表。功能還是一個 map 。)以本身為 key,以目標為 value。
主要方法是 get() 和 set(T a),set 之後在 map 裡維護一個threadLocal -> a,get 時將 a 返回。ThreadLocal 是一個特殊的容器。
2.原子類(AtomicInteger、AtomicBoolean……)
如果使用 atomic wrapper class 如 atomicInteger,或者使用自己保證原子的操作,則等同於 synchronized
//返回值為 boolean
AtomicInteger.compareAndSet(int expect,int update)
該方法可用於實現樂觀鎖,考慮文中最初提到的如下場景:a 給 b 付款10元,a 扣了 10 元,b 要加 10 元。此時 c 給 b 2 元,但是 b的加十元代碼約為:
if(b.value.compareAndSet(old, value)){
return ;
}else{
//try again
// if that fails, rollback and log
}
AtomicReference
對於 AtomicReference 來講,也許對象會出現,屬性丟失的情況,即 oldObject == current,但是 oldObject.getPropertyA != current.getPropertyA。
這時候,AtomicStampedReference 就派上用場了。這也是一個很常用的思路,即加上版本號
3.Lock類
lock: 在 java.util.concurrent 包內。共有三個實現:
- ReentrantLock
- ReentrantReadWriteLock.ReadLock
- ReentrantReadWriteLock.WriteLock
主要目的是和 synchronized 一樣, 兩者都是為了解決同步問題,處理資源爭端而產生的技術。功能類似但有一些區別。
區別如下:
1.lock 更靈活,可以自由定義多把鎖的枷鎖解鎖順(synchronized 要按照先加的後解順序)
2.提供多種加鎖方案,lock 阻塞式, trylock 無阻塞式, lockInterruptily 可打斷式, 還有 trylock 的帶超時時間版本
3.本質上和監視器鎖(即 synchronized 是一樣的)
4.能力越大,責任越大,必須控制好加鎖和解鎖,否則會導致災難。
5.和 Condition 類的結合。
6.性能更高,對比如下圖:
作者 | 納達丶無忌
如果對什麼是線程、什麼是進程仍存有疑惑,請先 Google 之,因為這兩個概念不在本文的範圍之內。
用多線程只有一個目的,那就是更好的利用 CPU 的資源,因為所有的多線程代碼都可以用單線程來實現。說這個話其實只有一半對,因為反應“多角色”的程序代碼,最起碼每個角色要給他一個線程吧,否則連實際場景都無法模擬,當然也沒法說能用單線程來實現:比如最常見的“生產者,消費者模型”。
很多人都對其中的一些概念不夠明確,如同步、併發等等,讓我們先建立一個數據字典,以免產生誤會。
- 多線程:指的是這個程序(一個進程)運行時產生了不止一個線程
- 並行與併發:
- 並行:多個 CPU 實例或者多臺機器同時執行一段處理邏輯,是真正的同時。
- 併發:通過 CPU 調度算法,讓用戶看上去同時執行,實際上從 CPU 操作層面不是真正的同時。併發往往在場景中有公用的資源,那麼針對這個公用的資源往往產生瓶頸,我們會用 TPS 或者 QPS 來反應這個系統的處理能力。
併發與並行
- 線程安全:經常用來描繪一段代碼。指在併發的情況之下,該代碼經過多線程使用,線程的調度順序不影響任何結果。這個時候使用多線程,我們只需要關注系統的內存,CPU 是不是夠用即可。反過來,線程不安全就意味著線程的調度順序會影響最終結果,如不加事務的轉賬代碼:
void transferMoney(User from, User to, float amount){
to.setMoney(to.getBalance() + amount);
from.setMoney(from.getBalance() - amount);
}
- 同步:Java 中的同步指的是通過人為的控制和調度,保證共享資源的多線程訪問成為線程安全,來保證結果的準確。如上面的代碼簡單加入 @synchronized 關鍵字。在保證結果準確的同時,提高性能,才是優秀的程序。線程安全的優先級高於性能。
好了,讓我們開始吧。我準備分成幾部分來總結涉及到多線程的內容:
1. 紮好馬步:線程的狀態
2. 內功心法:每個對象都有的方法(機制)
3. 太祖長拳:基本線程類
4. 九陰真經:高級多線程控制類
紮好馬步:線程的狀態
先來兩張圖:
線程狀態
線程狀態轉換
各種狀態一目瞭然,值得一提的是 "Blocked" 和 "Waiting" 這兩個狀態的區別:
- 線程在 Running 的過程中可能會遇到阻塞 (Blocked) 情況
- 對 Running 狀態的線程加同步鎖 (Synchronized) 使其進入 (lock blocked pool),同步鎖被釋放進入可運行狀 (Runnable)。從 jdk 源碼註釋來看,blocked 指的是對 monitor 的等待(可以參考下文的圖)即該線程位於等待區。
- 線程在 Running 的過程中可能會遇到等待(Waiting)情況
- 線程可以主動調用 object.wait 或者 sleep,或者 join(join內部調用的是 sleep ,所以可看成 sleep 的一種)進入。從 jdk 源碼註釋來看,Waiting 是等待另一個線程完成某一個操作,如 join 等待另一個完成執行,object.wait() 等待object.notify() 方法執行。
Waiting 狀態和 Blocked 狀態有點費解,我個人的理解是:Blocked 其實也是一種 wait ,等待的是 monitor ,但是和Waiting 狀態不一樣,舉個例子,有三個線程進入了同步塊,其中兩個調用了 object.wait(),進入了 Waiting 狀態,這時第三個調用了 object.notifyAll() ,這時候前兩個線程就一個轉移到了 Runnable,一個轉移到了 Blocked。
從下文的 monitor 結構圖來區別:每個 Monitor 在某個時刻,只能被一個線程擁有,該線程就是 “Active Thread”,而其它線程都是 “Waiting Thread”,分別在兩個隊列 “ Entry Set” 和 “Wait Set” 裡面等候。在 “Entry Set” 中等待的線程狀態 Blocked,從 jstack 的dump 中來看是 “Waiting for monitor entry”,而在 “Wait Set” 中等待的線程狀態是 Waiting,表現在 jstack 的 dump 中是 “in Object.wait()”。
此外,在 runnable 狀態的線程是處於被調度的線程,此時的調度順序是不一定的。Thread 類中的 yield 方法可以讓一個 running 狀態的線程轉入 runnable。
內功心法:每個對象都有的方法(機制)
synchronized, wait, notify 是任何對象都具有的同步工具。讓我們先來了解他們
monitor
他們是應用於同步問題的人工線程調度工具。講其本質,首先就要明確 monitor 的概念,Java 中的每個對象都有一個監視器,來監測併發代碼的重入。在非多線程編碼時該監視器不發揮作用,反之如果在 synchronized 範圍內,監視器發揮作用。
wait/notify 必須存在於 synchronized 塊中。並且,這三個關鍵字針對的是同一個監視器(某對象的監視器)。這意味著 wait之後,其他線程可以進入同步塊執行。
當某代碼並不持有監視器的使用權時(如圖中5的狀態,即脫離同步塊)去 wait 或 notify,會拋出java.lang.IllegalMonitorStateException。
也包括在 synchronized 塊中去調用另一個對象的 wait/notify,因為不同對象的監視器不同,同樣會拋出此異常。
再講用法:
- synchronized 單獨使用:
- 代碼塊:如下,在多線程環境下,synchronized 塊中的方法獲取了 lock 實例的 monitor,如果實例相同,那麼只有一個線程能執行該塊內容
public class Thread1 implements Runnable {
Object lock;
public void run() {
synchronized(lock){
..do something
}
}
}
- 直接用於方法:相當於上面代碼中用 lock 來鎖定的效果,實際獲取的是 Thread1 類的 monitor。更進一步,如果修飾的是 static 方法,則鎖定該類所有實例
public class Thread1 implements Runnable {
public synchronized void run() {
..do something
}
}
- synchronized, wait, notify 結合:典型場景生產者消費者問題
/**
* 生產者生產出來的產品交給店員
*/
public synchronized void produce()
{
if(this.product >= MAX_PRODUCT)
{
try
{
wait();
System.out.println("產品已滿,請稍候再生產");
}
catch(InterruptedException e)
{
e.printStackTrace () ;
}
return;
}
this.product++;
System.out.println("生產者生產第" + this.product + "個產品.");
notifyAll(); //通知等待區的消費者可以取出產品了
}
/**
* 消費者從店員取產品
*/
public synchronized void consume()
{
if(this.product <= MIN_PRODUCT)
{
try
{
wait();
System.out.println("缺貨,稍候再取");
}
catch (InterruptedException e)
{
e.printStackTrace();
}
return;
}
System.out.println("消費者取走了第" + this.product + "個產品.");
this.product--;
notifyAll(); //通知等待去的生產者可以生產產品了
}
volatile
多線程的內存模型:main memory(主存)、working memory(線程棧),在處理數據時,線程會把值從主存 load 到本地棧,完成操作後再 save 回去 (volatile 關鍵詞的作用:每次針對該變量的操作都激發一次 load and save) 。
volatile
針對多線程使用的變量如果不是 volatile 或者 final 修飾的,很有可能產生不可預知的結果(另一個線程修改了這個值,但是之後在某線程看到的是修改之前的值)。其實道理上講同一實例的同一屬性本身只有一個副本。但是多線程是會緩存值的,本質上,volatile 就是不去緩存,直接取值。在線程安全的情況下加 volatile 會犧牲性能。
太祖長拳:基本線程類
基本線程類指的是 Thread 類,Runnable 接口,Callable 接口
Thread 類實現了 Runnable 接口,啟動一個線程的方法:
MyThread my = new MyThread();
my.start();
Thread類相關方法
//當前線程可轉讓 cpu 控制權,讓別的就緒狀態線程運行(切換)
public static Thread.yield()
//暫停一段時間
public static Thread.sleep()
//在一個線程中調用 other.join(),將等待other執行完後才繼續本線程。
public join()
//後兩個函數皆可以被打斷
public interrupte()
關於中斷:它並不像 stop 方法那樣會中斷一個正在運行的線程。線程會不時地檢測中斷標識位,以判斷線程是否應該被中斷(中斷標識值是否為 true )。終端只會影響到 wait 狀態、sleep 狀態和 join 狀態。被打斷的線程會拋出 InterruptedException。
Thread.interrupted() 檢查當前線程是否發生中斷,返回boolean
synchronized 在獲鎖的過程中是不能被中斷的。
中斷是一個狀態!interrupt()方法只是將這個狀態置為 true 而已。所以說正常運行的程序不去檢測狀態,就不會終止,而 wait 等阻塞方法會去檢查並拋出異常。如果在正常運行的程序中添加while(!Thread.interrupted()) ,則同樣可以在中斷後離開代碼體
Thread類最佳實踐:
寫的時候最好要設置線程名稱 Thread.name,並設置線程組 ThreadGroup,目的是方便管理。在出現問題的時候,打印線程棧 (jstack -pid) 一眼就可以看出是哪個線程出的問題,這個線程是幹什麼的。
如何獲取線程中的異常
不能用try,catch來獲取線程中的異常
Runnable
與 Thread 類似
Callable
future 模式:併發模式的一種,可以有兩種形式,即無阻塞和阻塞,分別是 isDone 和 get。其中 Future 對象用來存放該線程的返回值以及狀態
ExecutorService e = Executors.newFixedThreadPool(3);
//submit 方法有多重參數版本,及支持 callable 也能夠支持runnable 接口類型.
Future future = e.submit(new myCallable());
future.isDone() //return true,false 無阻塞
future.get() // return 返回值,阻塞直到該線程運行結束
九陰真經:高級多線程控制類
以上都屬於內功心法,接下來是實際項目中常用到的工具了,Java1.5 提供了一個非常高效實用的多線程包: java.util.concurrent, 提供了大量高級工具,可以幫助開發者編寫高效、易維護、結構清晰的 Java 多線程程序。
1.ThreadLocal類
用處:保存線程的獨立變量。對一個線程類(繼承自 Thread )
當使用 ThreadLocal 維護變量時,ThreadLocal 為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。常用於用戶登錄控制,如記錄 session 信息。
實現:每個Thread 都持有一個 TreadLocalMap 類型的變量(該類是一個輕量級的 Map,功能與 map 一樣,區別是桶裡放的是 entry 而不是 entry 的鏈表。功能還是一個 map 。)以本身為 key,以目標為 value。
主要方法是 get() 和 set(T a),set 之後在 map 裡維護一個threadLocal -> a,get 時將 a 返回。ThreadLocal 是一個特殊的容器。
2.原子類(AtomicInteger、AtomicBoolean……)
如果使用 atomic wrapper class 如 atomicInteger,或者使用自己保證原子的操作,則等同於 synchronized
//返回值為 boolean
AtomicInteger.compareAndSet(int expect,int update)
該方法可用於實現樂觀鎖,考慮文中最初提到的如下場景:a 給 b 付款10元,a 扣了 10 元,b 要加 10 元。此時 c 給 b 2 元,但是 b的加十元代碼約為:
if(b.value.compareAndSet(old, value)){
return ;
}else{
//try again
// if that fails, rollback and log
}
AtomicReference
對於 AtomicReference 來講,也許對象會出現,屬性丟失的情況,即 oldObject == current,但是 oldObject.getPropertyA != current.getPropertyA。
這時候,AtomicStampedReference 就派上用場了。這也是一個很常用的思路,即加上版本號
3.Lock類
lock: 在 java.util.concurrent 包內。共有三個實現:
- ReentrantLock
- ReentrantReadWriteLock.ReadLock
- ReentrantReadWriteLock.WriteLock
主要目的是和 synchronized 一樣, 兩者都是為了解決同步問題,處理資源爭端而產生的技術。功能類似但有一些區別。
區別如下:
1.lock 更靈活,可以自由定義多把鎖的枷鎖解鎖順(synchronized 要按照先加的後解順序)
2.提供多種加鎖方案,lock 阻塞式, trylock 無阻塞式, lockInterruptily 可打斷式, 還有 trylock 的帶超時時間版本
3.本質上和監視器鎖(即 synchronized 是一樣的)
4.能力越大,責任越大,必須控制好加鎖和解鎖,否則會導致災難。
5.和 Condition 類的結合。
6.性能更高,對比如下圖:
synchronized和Lock性能對比
ReentrantLock
可重入的意義在於持有鎖的線程可以繼續持有,並且要釋放對等的次數後才真正釋放該鎖。
使用方法是:
1.先 new 一個實例
static ReentrantLock r=new ReentrantLock();
2.加鎖
r.lock()或 r.lockInterruptibly();
此處也是個不同,後者可被打斷。當 a 線程 lock 後,b 線程阻塞,此時如果是 lockInterruptibly,那麼在調用 b.interrupt() 之後,b 線程退出阻塞,並放棄對資源的爭搶,進入 catch 塊。(如果使用後者,必須 throw interruptable exception 或 catch)
3.釋放鎖
r.unlock()
必須做!何為必須做呢,要放在 finally 裡面。以防止異常跳出了正常流程,導致災難。這裡補充一個小知識點,finally 是可以信任的:經過測試,哪怕是發生了 OutofMemoryError ,finally 塊中的語句執行也能夠得到保證。
ReentrantReadWriteLock
可重入讀寫鎖(讀寫鎖的一個實現)
ReentrantReadWriteLock lock = new ReentrantReadWriteLock()
ReadLock r = lock.readLock();
WriteLock w = lock.writeLock();
兩者都有 lock,unlock 方法。寫寫,寫讀互斥;讀讀不互斥。可以實現併發讀的高效線程安全代碼
4.容器類
這裡就討論比較常用的兩個:
- BlockingQueue
- ConcurrentHashMap
BlockingQueue
阻塞隊列。該類是 java.util.concurrent 包下的重要類,通過對 Queue 的學習可以得知,這個 queue 是單向隊列,可以在隊列頭添加元素和在隊尾刪除或取出元素。類似於一個管道,特別適用於先進先出策略的一些應用場景。普通的 queue 接口主要實現有 PriorityQueue(優先隊列),有興趣可以研究
BlockingQueue 在隊列的基礎上添加了多線程協作的功能:
作者 | 納達丶無忌
如果對什麼是線程、什麼是進程仍存有疑惑,請先 Google 之,因為這兩個概念不在本文的範圍之內。
用多線程只有一個目的,那就是更好的利用 CPU 的資源,因為所有的多線程代碼都可以用單線程來實現。說這個話其實只有一半對,因為反應“多角色”的程序代碼,最起碼每個角色要給他一個線程吧,否則連實際場景都無法模擬,當然也沒法說能用單線程來實現:比如最常見的“生產者,消費者模型”。
很多人都對其中的一些概念不夠明確,如同步、併發等等,讓我們先建立一個數據字典,以免產生誤會。
- 多線程:指的是這個程序(一個進程)運行時產生了不止一個線程
- 並行與併發:
- 並行:多個 CPU 實例或者多臺機器同時執行一段處理邏輯,是真正的同時。
- 併發:通過 CPU 調度算法,讓用戶看上去同時執行,實際上從 CPU 操作層面不是真正的同時。併發往往在場景中有公用的資源,那麼針對這個公用的資源往往產生瓶頸,我們會用 TPS 或者 QPS 來反應這個系統的處理能力。
併發與並行
- 線程安全:經常用來描繪一段代碼。指在併發的情況之下,該代碼經過多線程使用,線程的調度順序不影響任何結果。這個時候使用多線程,我們只需要關注系統的內存,CPU 是不是夠用即可。反過來,線程不安全就意味著線程的調度順序會影響最終結果,如不加事務的轉賬代碼:
void transferMoney(User from, User to, float amount){
to.setMoney(to.getBalance() + amount);
from.setMoney(from.getBalance() - amount);
}
- 同步:Java 中的同步指的是通過人為的控制和調度,保證共享資源的多線程訪問成為線程安全,來保證結果的準確。如上面的代碼簡單加入 @synchronized 關鍵字。在保證結果準確的同時,提高性能,才是優秀的程序。線程安全的優先級高於性能。
好了,讓我們開始吧。我準備分成幾部分來總結涉及到多線程的內容:
1. 紮好馬步:線程的狀態
2. 內功心法:每個對象都有的方法(機制)
3. 太祖長拳:基本線程類
4. 九陰真經:高級多線程控制類
紮好馬步:線程的狀態
先來兩張圖:
線程狀態
線程狀態轉換
各種狀態一目瞭然,值得一提的是 "Blocked" 和 "Waiting" 這兩個狀態的區別:
- 線程在 Running 的過程中可能會遇到阻塞 (Blocked) 情況
- 對 Running 狀態的線程加同步鎖 (Synchronized) 使其進入 (lock blocked pool),同步鎖被釋放進入可運行狀 (Runnable)。從 jdk 源碼註釋來看,blocked 指的是對 monitor 的等待(可以參考下文的圖)即該線程位於等待區。
- 線程在 Running 的過程中可能會遇到等待(Waiting)情況
- 線程可以主動調用 object.wait 或者 sleep,或者 join(join內部調用的是 sleep ,所以可看成 sleep 的一種)進入。從 jdk 源碼註釋來看,Waiting 是等待另一個線程完成某一個操作,如 join 等待另一個完成執行,object.wait() 等待object.notify() 方法執行。
Waiting 狀態和 Blocked 狀態有點費解,我個人的理解是:Blocked 其實也是一種 wait ,等待的是 monitor ,但是和Waiting 狀態不一樣,舉個例子,有三個線程進入了同步塊,其中兩個調用了 object.wait(),進入了 Waiting 狀態,這時第三個調用了 object.notifyAll() ,這時候前兩個線程就一個轉移到了 Runnable,一個轉移到了 Blocked。
從下文的 monitor 結構圖來區別:每個 Monitor 在某個時刻,只能被一個線程擁有,該線程就是 “Active Thread”,而其它線程都是 “Waiting Thread”,分別在兩個隊列 “ Entry Set” 和 “Wait Set” 裡面等候。在 “Entry Set” 中等待的線程狀態 Blocked,從 jstack 的dump 中來看是 “Waiting for monitor entry”,而在 “Wait Set” 中等待的線程狀態是 Waiting,表現在 jstack 的 dump 中是 “in Object.wait()”。
此外,在 runnable 狀態的線程是處於被調度的線程,此時的調度順序是不一定的。Thread 類中的 yield 方法可以讓一個 running 狀態的線程轉入 runnable。
內功心法:每個對象都有的方法(機制)
synchronized, wait, notify 是任何對象都具有的同步工具。讓我們先來了解他們
monitor
他們是應用於同步問題的人工線程調度工具。講其本質,首先就要明確 monitor 的概念,Java 中的每個對象都有一個監視器,來監測併發代碼的重入。在非多線程編碼時該監視器不發揮作用,反之如果在 synchronized 範圍內,監視器發揮作用。
wait/notify 必須存在於 synchronized 塊中。並且,這三個關鍵字針對的是同一個監視器(某對象的監視器)。這意味著 wait之後,其他線程可以進入同步塊執行。
當某代碼並不持有監視器的使用權時(如圖中5的狀態,即脫離同步塊)去 wait 或 notify,會拋出java.lang.IllegalMonitorStateException。
也包括在 synchronized 塊中去調用另一個對象的 wait/notify,因為不同對象的監視器不同,同樣會拋出此異常。
再講用法:
- synchronized 單獨使用:
- 代碼塊:如下,在多線程環境下,synchronized 塊中的方法獲取了 lock 實例的 monitor,如果實例相同,那麼只有一個線程能執行該塊內容
public class Thread1 implements Runnable {
Object lock;
public void run() {
synchronized(lock){
..do something
}
}
}
- 直接用於方法:相當於上面代碼中用 lock 來鎖定的效果,實際獲取的是 Thread1 類的 monitor。更進一步,如果修飾的是 static 方法,則鎖定該類所有實例
public class Thread1 implements Runnable {
public synchronized void run() {
..do something
}
}
- synchronized, wait, notify 結合:典型場景生產者消費者問題
/**
* 生產者生產出來的產品交給店員
*/
public synchronized void produce()
{
if(this.product >= MAX_PRODUCT)
{
try
{
wait();
System.out.println("產品已滿,請稍候再生產");
}
catch(InterruptedException e)
{
e.printStackTrace () ;
}
return;
}
this.product++;
System.out.println("生產者生產第" + this.product + "個產品.");
notifyAll(); //通知等待區的消費者可以取出產品了
}
/**
* 消費者從店員取產品
*/
public synchronized void consume()
{
if(this.product <= MIN_PRODUCT)
{
try
{
wait();
System.out.println("缺貨,稍候再取");
}
catch (InterruptedException e)
{
e.printStackTrace();
}
return;
}
System.out.println("消費者取走了第" + this.product + "個產品.");
this.product--;
notifyAll(); //通知等待去的生產者可以生產產品了
}
volatile
多線程的內存模型:main memory(主存)、working memory(線程棧),在處理數據時,線程會把值從主存 load 到本地棧,完成操作後再 save 回去 (volatile 關鍵詞的作用:每次針對該變量的操作都激發一次 load and save) 。
volatile
針對多線程使用的變量如果不是 volatile 或者 final 修飾的,很有可能產生不可預知的結果(另一個線程修改了這個值,但是之後在某線程看到的是修改之前的值)。其實道理上講同一實例的同一屬性本身只有一個副本。但是多線程是會緩存值的,本質上,volatile 就是不去緩存,直接取值。在線程安全的情況下加 volatile 會犧牲性能。
太祖長拳:基本線程類
基本線程類指的是 Thread 類,Runnable 接口,Callable 接口
Thread 類實現了 Runnable 接口,啟動一個線程的方法:
MyThread my = new MyThread();
my.start();
Thread類相關方法
//當前線程可轉讓 cpu 控制權,讓別的就緒狀態線程運行(切換)
public static Thread.yield()
//暫停一段時間
public static Thread.sleep()
//在一個線程中調用 other.join(),將等待other執行完後才繼續本線程。
public join()
//後兩個函數皆可以被打斷
public interrupte()
關於中斷:它並不像 stop 方法那樣會中斷一個正在運行的線程。線程會不時地檢測中斷標識位,以判斷線程是否應該被中斷(中斷標識值是否為 true )。終端只會影響到 wait 狀態、sleep 狀態和 join 狀態。被打斷的線程會拋出 InterruptedException。
Thread.interrupted() 檢查當前線程是否發生中斷,返回boolean
synchronized 在獲鎖的過程中是不能被中斷的。
中斷是一個狀態!interrupt()方法只是將這個狀態置為 true 而已。所以說正常運行的程序不去檢測狀態,就不會終止,而 wait 等阻塞方法會去檢查並拋出異常。如果在正常運行的程序中添加while(!Thread.interrupted()) ,則同樣可以在中斷後離開代碼體
Thread類最佳實踐:
寫的時候最好要設置線程名稱 Thread.name,並設置線程組 ThreadGroup,目的是方便管理。在出現問題的時候,打印線程棧 (jstack -pid) 一眼就可以看出是哪個線程出的問題,這個線程是幹什麼的。
如何獲取線程中的異常
不能用try,catch來獲取線程中的異常
Runnable
與 Thread 類似
Callable
future 模式:併發模式的一種,可以有兩種形式,即無阻塞和阻塞,分別是 isDone 和 get。其中 Future 對象用來存放該線程的返回值以及狀態
ExecutorService e = Executors.newFixedThreadPool(3);
//submit 方法有多重參數版本,及支持 callable 也能夠支持runnable 接口類型.
Future future = e.submit(new myCallable());
future.isDone() //return true,false 無阻塞
future.get() // return 返回值,阻塞直到該線程運行結束
九陰真經:高級多線程控制類
以上都屬於內功心法,接下來是實際項目中常用到的工具了,Java1.5 提供了一個非常高效實用的多線程包: java.util.concurrent, 提供了大量高級工具,可以幫助開發者編寫高效、易維護、結構清晰的 Java 多線程程序。
1.ThreadLocal類
用處:保存線程的獨立變量。對一個線程類(繼承自 Thread )
當使用 ThreadLocal 維護變量時,ThreadLocal 為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。常用於用戶登錄控制,如記錄 session 信息。
實現:每個Thread 都持有一個 TreadLocalMap 類型的變量(該類是一個輕量級的 Map,功能與 map 一樣,區別是桶裡放的是 entry 而不是 entry 的鏈表。功能還是一個 map 。)以本身為 key,以目標為 value。
主要方法是 get() 和 set(T a),set 之後在 map 裡維護一個threadLocal -> a,get 時將 a 返回。ThreadLocal 是一個特殊的容器。
2.原子類(AtomicInteger、AtomicBoolean……)
如果使用 atomic wrapper class 如 atomicInteger,或者使用自己保證原子的操作,則等同於 synchronized
//返回值為 boolean
AtomicInteger.compareAndSet(int expect,int update)
該方法可用於實現樂觀鎖,考慮文中最初提到的如下場景:a 給 b 付款10元,a 扣了 10 元,b 要加 10 元。此時 c 給 b 2 元,但是 b的加十元代碼約為:
if(b.value.compareAndSet(old, value)){
return ;
}else{
//try again
// if that fails, rollback and log
}
AtomicReference
對於 AtomicReference 來講,也許對象會出現,屬性丟失的情況,即 oldObject == current,但是 oldObject.getPropertyA != current.getPropertyA。
這時候,AtomicStampedReference 就派上用場了。這也是一個很常用的思路,即加上版本號
3.Lock類
lock: 在 java.util.concurrent 包內。共有三個實現:
- ReentrantLock
- ReentrantReadWriteLock.ReadLock
- ReentrantReadWriteLock.WriteLock
主要目的是和 synchronized 一樣, 兩者都是為了解決同步問題,處理資源爭端而產生的技術。功能類似但有一些區別。
區別如下:
1.lock 更靈活,可以自由定義多把鎖的枷鎖解鎖順(synchronized 要按照先加的後解順序)
2.提供多種加鎖方案,lock 阻塞式, trylock 無阻塞式, lockInterruptily 可打斷式, 還有 trylock 的帶超時時間版本
3.本質上和監視器鎖(即 synchronized 是一樣的)
4.能力越大,責任越大,必須控制好加鎖和解鎖,否則會導致災難。
5.和 Condition 類的結合。
6.性能更高,對比如下圖:
synchronized和Lock性能對比
ReentrantLock
可重入的意義在於持有鎖的線程可以繼續持有,並且要釋放對等的次數後才真正釋放該鎖。
使用方法是:
1.先 new 一個實例
static ReentrantLock r=new ReentrantLock();
2.加鎖
r.lock()或 r.lockInterruptibly();
此處也是個不同,後者可被打斷。當 a 線程 lock 後,b 線程阻塞,此時如果是 lockInterruptibly,那麼在調用 b.interrupt() 之後,b 線程退出阻塞,並放棄對資源的爭搶,進入 catch 塊。(如果使用後者,必須 throw interruptable exception 或 catch)
3.釋放鎖
r.unlock()
必須做!何為必須做呢,要放在 finally 裡面。以防止異常跳出了正常流程,導致災難。這裡補充一個小知識點,finally 是可以信任的:經過測試,哪怕是發生了 OutofMemoryError ,finally 塊中的語句執行也能夠得到保證。
ReentrantReadWriteLock
可重入讀寫鎖(讀寫鎖的一個實現)
ReentrantReadWriteLock lock = new ReentrantReadWriteLock()
ReadLock r = lock.readLock();
WriteLock w = lock.writeLock();
兩者都有 lock,unlock 方法。寫寫,寫讀互斥;讀讀不互斥。可以實現併發讀的高效線程安全代碼
4.容器類
這裡就討論比較常用的兩個:
- BlockingQueue
- ConcurrentHashMap
BlockingQueue
阻塞隊列。該類是 java.util.concurrent 包下的重要類,通過對 Queue 的學習可以得知,這個 queue 是單向隊列,可以在隊列頭添加元素和在隊尾刪除或取出元素。類似於一個管道,特別適用於先進先出策略的一些應用場景。普通的 queue 接口主要實現有 PriorityQueue(優先隊列),有興趣可以研究
BlockingQueue 在隊列的基礎上添加了多線程協作的功能:
lockingQueue
除了傳統的 queue 功能(表格左邊的兩列)之外,還提供了阻塞接口 put 和 take,帶超時功能的阻塞接口 offer 和 poll。put 會在隊列滿的時候阻塞,直到有空間時被喚醒;take 在隊 列空的時候阻塞,直到有東西拿的時候才被喚醒。用於生產者-消費者模型尤其好用,堪稱神器。
常見的阻塞隊列有:
- ArrayListBlockingQueue
- LinkedListBlockingQueue
- DelayQueue
- SynchronousQueue
** ConcurrentHashMap**
高效的線程安全哈希 map。請對比 hashTable , concurrentHashMap, HashMap
5.管理類
管理類的概念比較泛,用於管理線程,本身不是多線程的,但提供了一些機制來利用上述的工具做一些封裝。
瞭解到的值得一提的管理類:ThreadPoolExecutor 和 JMX框架下的系統級管理類 ThreadMXBean
ThreadPoolExecutor
如果不瞭解這個類,應該瞭解前面提到的 ExecutorService,開一個自己的線程池非常方便
ExecutorService e = Executors.newCachedThreadPool();
ExecutorService e =Executors.newSingleThreadExecutor();
ExecutorService e = Executors.newFixedThreadPool(3);
// 第一種是可變大小線程池,按照任務數來分配線程,
// 第二種是單線程池,相當於 FixedThreadPool(1)
// 第三種是固定大小線程池。
// 然後運行
e.execute(new MyRunnableImpl());
該類內部是通過 ThreadPoolExecutor 實現的,掌握該類有助於理解線程池的管理,本質上,他們都是 ThreadPoolExecutor 類的各種實現版本。請參見 javadoc:
作者 | 納達丶無忌
如果對什麼是線程、什麼是進程仍存有疑惑,請先 Google 之,因為這兩個概念不在本文的範圍之內。
用多線程只有一個目的,那就是更好的利用 CPU 的資源,因為所有的多線程代碼都可以用單線程來實現。說這個話其實只有一半對,因為反應“多角色”的程序代碼,最起碼每個角色要給他一個線程吧,否則連實際場景都無法模擬,當然也沒法說能用單線程來實現:比如最常見的“生產者,消費者模型”。
很多人都對其中的一些概念不夠明確,如同步、併發等等,讓我們先建立一個數據字典,以免產生誤會。
- 多線程:指的是這個程序(一個進程)運行時產生了不止一個線程
- 並行與併發:
- 並行:多個 CPU 實例或者多臺機器同時執行一段處理邏輯,是真正的同時。
- 併發:通過 CPU 調度算法,讓用戶看上去同時執行,實際上從 CPU 操作層面不是真正的同時。併發往往在場景中有公用的資源,那麼針對這個公用的資源往往產生瓶頸,我們會用 TPS 或者 QPS 來反應這個系統的處理能力。
併發與並行
- 線程安全:經常用來描繪一段代碼。指在併發的情況之下,該代碼經過多線程使用,線程的調度順序不影響任何結果。這個時候使用多線程,我們只需要關注系統的內存,CPU 是不是夠用即可。反過來,線程不安全就意味著線程的調度順序會影響最終結果,如不加事務的轉賬代碼:
void transferMoney(User from, User to, float amount){
to.setMoney(to.getBalance() + amount);
from.setMoney(from.getBalance() - amount);
}
- 同步:Java 中的同步指的是通過人為的控制和調度,保證共享資源的多線程訪問成為線程安全,來保證結果的準確。如上面的代碼簡單加入 @synchronized 關鍵字。在保證結果準確的同時,提高性能,才是優秀的程序。線程安全的優先級高於性能。
好了,讓我們開始吧。我準備分成幾部分來總結涉及到多線程的內容:
1. 紮好馬步:線程的狀態
2. 內功心法:每個對象都有的方法(機制)
3. 太祖長拳:基本線程類
4. 九陰真經:高級多線程控制類
紮好馬步:線程的狀態
先來兩張圖:
線程狀態
線程狀態轉換
各種狀態一目瞭然,值得一提的是 "Blocked" 和 "Waiting" 這兩個狀態的區別:
- 線程在 Running 的過程中可能會遇到阻塞 (Blocked) 情況
- 對 Running 狀態的線程加同步鎖 (Synchronized) 使其進入 (lock blocked pool),同步鎖被釋放進入可運行狀 (Runnable)。從 jdk 源碼註釋來看,blocked 指的是對 monitor 的等待(可以參考下文的圖)即該線程位於等待區。
- 線程在 Running 的過程中可能會遇到等待(Waiting)情況
- 線程可以主動調用 object.wait 或者 sleep,或者 join(join內部調用的是 sleep ,所以可看成 sleep 的一種)進入。從 jdk 源碼註釋來看,Waiting 是等待另一個線程完成某一個操作,如 join 等待另一個完成執行,object.wait() 等待object.notify() 方法執行。
Waiting 狀態和 Blocked 狀態有點費解,我個人的理解是:Blocked 其實也是一種 wait ,等待的是 monitor ,但是和Waiting 狀態不一樣,舉個例子,有三個線程進入了同步塊,其中兩個調用了 object.wait(),進入了 Waiting 狀態,這時第三個調用了 object.notifyAll() ,這時候前兩個線程就一個轉移到了 Runnable,一個轉移到了 Blocked。
從下文的 monitor 結構圖來區別:每個 Monitor 在某個時刻,只能被一個線程擁有,該線程就是 “Active Thread”,而其它線程都是 “Waiting Thread”,分別在兩個隊列 “ Entry Set” 和 “Wait Set” 裡面等候。在 “Entry Set” 中等待的線程狀態 Blocked,從 jstack 的dump 中來看是 “Waiting for monitor entry”,而在 “Wait Set” 中等待的線程狀態是 Waiting,表現在 jstack 的 dump 中是 “in Object.wait()”。
此外,在 runnable 狀態的線程是處於被調度的線程,此時的調度順序是不一定的。Thread 類中的 yield 方法可以讓一個 running 狀態的線程轉入 runnable。
內功心法:每個對象都有的方法(機制)
synchronized, wait, notify 是任何對象都具有的同步工具。讓我們先來了解他們
monitor
他們是應用於同步問題的人工線程調度工具。講其本質,首先就要明確 monitor 的概念,Java 中的每個對象都有一個監視器,來監測併發代碼的重入。在非多線程編碼時該監視器不發揮作用,反之如果在 synchronized 範圍內,監視器發揮作用。
wait/notify 必須存在於 synchronized 塊中。並且,這三個關鍵字針對的是同一個監視器(某對象的監視器)。這意味著 wait之後,其他線程可以進入同步塊執行。
當某代碼並不持有監視器的使用權時(如圖中5的狀態,即脫離同步塊)去 wait 或 notify,會拋出java.lang.IllegalMonitorStateException。
也包括在 synchronized 塊中去調用另一個對象的 wait/notify,因為不同對象的監視器不同,同樣會拋出此異常。
再講用法:
- synchronized 單獨使用:
- 代碼塊:如下,在多線程環境下,synchronized 塊中的方法獲取了 lock 實例的 monitor,如果實例相同,那麼只有一個線程能執行該塊內容
public class Thread1 implements Runnable {
Object lock;
public void run() {
synchronized(lock){
..do something
}
}
}
- 直接用於方法:相當於上面代碼中用 lock 來鎖定的效果,實際獲取的是 Thread1 類的 monitor。更進一步,如果修飾的是 static 方法,則鎖定該類所有實例
public class Thread1 implements Runnable {
public synchronized void run() {
..do something
}
}
- synchronized, wait, notify 結合:典型場景生產者消費者問題
/**
* 生產者生產出來的產品交給店員
*/
public synchronized void produce()
{
if(this.product >= MAX_PRODUCT)
{
try
{
wait();
System.out.println("產品已滿,請稍候再生產");
}
catch(InterruptedException e)
{
e.printStackTrace () ;
}
return;
}
this.product++;
System.out.println("生產者生產第" + this.product + "個產品.");
notifyAll(); //通知等待區的消費者可以取出產品了
}
/**
* 消費者從店員取產品
*/
public synchronized void consume()
{
if(this.product <= MIN_PRODUCT)
{
try
{
wait();
System.out.println("缺貨,稍候再取");
}
catch (InterruptedException e)
{
e.printStackTrace();
}
return;
}
System.out.println("消費者取走了第" + this.product + "個產品.");
this.product--;
notifyAll(); //通知等待去的生產者可以生產產品了
}
volatile
多線程的內存模型:main memory(主存)、working memory(線程棧),在處理數據時,線程會把值從主存 load 到本地棧,完成操作後再 save 回去 (volatile 關鍵詞的作用:每次針對該變量的操作都激發一次 load and save) 。
volatile
針對多線程使用的變量如果不是 volatile 或者 final 修飾的,很有可能產生不可預知的結果(另一個線程修改了這個值,但是之後在某線程看到的是修改之前的值)。其實道理上講同一實例的同一屬性本身只有一個副本。但是多線程是會緩存值的,本質上,volatile 就是不去緩存,直接取值。在線程安全的情況下加 volatile 會犧牲性能。
太祖長拳:基本線程類
基本線程類指的是 Thread 類,Runnable 接口,Callable 接口
Thread 類實現了 Runnable 接口,啟動一個線程的方法:
MyThread my = new MyThread();
my.start();
Thread類相關方法
//當前線程可轉讓 cpu 控制權,讓別的就緒狀態線程運行(切換)
public static Thread.yield()
//暫停一段時間
public static Thread.sleep()
//在一個線程中調用 other.join(),將等待other執行完後才繼續本線程。
public join()
//後兩個函數皆可以被打斷
public interrupte()
關於中斷:它並不像 stop 方法那樣會中斷一個正在運行的線程。線程會不時地檢測中斷標識位,以判斷線程是否應該被中斷(中斷標識值是否為 true )。終端只會影響到 wait 狀態、sleep 狀態和 join 狀態。被打斷的線程會拋出 InterruptedException。
Thread.interrupted() 檢查當前線程是否發生中斷,返回boolean
synchronized 在獲鎖的過程中是不能被中斷的。
中斷是一個狀態!interrupt()方法只是將這個狀態置為 true 而已。所以說正常運行的程序不去檢測狀態,就不會終止,而 wait 等阻塞方法會去檢查並拋出異常。如果在正常運行的程序中添加while(!Thread.interrupted()) ,則同樣可以在中斷後離開代碼體
Thread類最佳實踐:
寫的時候最好要設置線程名稱 Thread.name,並設置線程組 ThreadGroup,目的是方便管理。在出現問題的時候,打印線程棧 (jstack -pid) 一眼就可以看出是哪個線程出的問題,這個線程是幹什麼的。
如何獲取線程中的異常
不能用try,catch來獲取線程中的異常
Runnable
與 Thread 類似
Callable
future 模式:併發模式的一種,可以有兩種形式,即無阻塞和阻塞,分別是 isDone 和 get。其中 Future 對象用來存放該線程的返回值以及狀態
ExecutorService e = Executors.newFixedThreadPool(3);
//submit 方法有多重參數版本,及支持 callable 也能夠支持runnable 接口類型.
Future future = e.submit(new myCallable());
future.isDone() //return true,false 無阻塞
future.get() // return 返回值,阻塞直到該線程運行結束
九陰真經:高級多線程控制類
以上都屬於內功心法,接下來是實際項目中常用到的工具了,Java1.5 提供了一個非常高效實用的多線程包: java.util.concurrent, 提供了大量高級工具,可以幫助開發者編寫高效、易維護、結構清晰的 Java 多線程程序。
1.ThreadLocal類
用處:保存線程的獨立變量。對一個線程類(繼承自 Thread )
當使用 ThreadLocal 維護變量時,ThreadLocal 為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。常用於用戶登錄控制,如記錄 session 信息。
實現:每個Thread 都持有一個 TreadLocalMap 類型的變量(該類是一個輕量級的 Map,功能與 map 一樣,區別是桶裡放的是 entry 而不是 entry 的鏈表。功能還是一個 map 。)以本身為 key,以目標為 value。
主要方法是 get() 和 set(T a),set 之後在 map 裡維護一個threadLocal -> a,get 時將 a 返回。ThreadLocal 是一個特殊的容器。
2.原子類(AtomicInteger、AtomicBoolean……)
如果使用 atomic wrapper class 如 atomicInteger,或者使用自己保證原子的操作,則等同於 synchronized
//返回值為 boolean
AtomicInteger.compareAndSet(int expect,int update)
該方法可用於實現樂觀鎖,考慮文中最初提到的如下場景:a 給 b 付款10元,a 扣了 10 元,b 要加 10 元。此時 c 給 b 2 元,但是 b的加十元代碼約為:
if(b.value.compareAndSet(old, value)){
return ;
}else{
//try again
// if that fails, rollback and log
}
AtomicReference
對於 AtomicReference 來講,也許對象會出現,屬性丟失的情況,即 oldObject == current,但是 oldObject.getPropertyA != current.getPropertyA。
這時候,AtomicStampedReference 就派上用場了。這也是一個很常用的思路,即加上版本號
3.Lock類
lock: 在 java.util.concurrent 包內。共有三個實現:
- ReentrantLock
- ReentrantReadWriteLock.ReadLock
- ReentrantReadWriteLock.WriteLock
主要目的是和 synchronized 一樣, 兩者都是為了解決同步問題,處理資源爭端而產生的技術。功能類似但有一些區別。
區別如下:
1.lock 更靈活,可以自由定義多把鎖的枷鎖解鎖順(synchronized 要按照先加的後解順序)
2.提供多種加鎖方案,lock 阻塞式, trylock 無阻塞式, lockInterruptily 可打斷式, 還有 trylock 的帶超時時間版本
3.本質上和監視器鎖(即 synchronized 是一樣的)
4.能力越大,責任越大,必須控制好加鎖和解鎖,否則會導致災難。
5.和 Condition 類的結合。
6.性能更高,對比如下圖:
synchronized和Lock性能對比
ReentrantLock
可重入的意義在於持有鎖的線程可以繼續持有,並且要釋放對等的次數後才真正釋放該鎖。
使用方法是:
1.先 new 一個實例
static ReentrantLock r=new ReentrantLock();
2.加鎖
r.lock()或 r.lockInterruptibly();
此處也是個不同,後者可被打斷。當 a 線程 lock 後,b 線程阻塞,此時如果是 lockInterruptibly,那麼在調用 b.interrupt() 之後,b 線程退出阻塞,並放棄對資源的爭搶,進入 catch 塊。(如果使用後者,必須 throw interruptable exception 或 catch)
3.釋放鎖
r.unlock()
必須做!何為必須做呢,要放在 finally 裡面。以防止異常跳出了正常流程,導致災難。這裡補充一個小知識點,finally 是可以信任的:經過測試,哪怕是發生了 OutofMemoryError ,finally 塊中的語句執行也能夠得到保證。
ReentrantReadWriteLock
可重入讀寫鎖(讀寫鎖的一個實現)
ReentrantReadWriteLock lock = new ReentrantReadWriteLock()
ReadLock r = lock.readLock();
WriteLock w = lock.writeLock();
兩者都有 lock,unlock 方法。寫寫,寫讀互斥;讀讀不互斥。可以實現併發讀的高效線程安全代碼
4.容器類
這裡就討論比較常用的兩個:
- BlockingQueue
- ConcurrentHashMap
BlockingQueue
阻塞隊列。該類是 java.util.concurrent 包下的重要類,通過對 Queue 的學習可以得知,這個 queue 是單向隊列,可以在隊列頭添加元素和在隊尾刪除或取出元素。類似於一個管道,特別適用於先進先出策略的一些應用場景。普通的 queue 接口主要實現有 PriorityQueue(優先隊列),有興趣可以研究
BlockingQueue 在隊列的基礎上添加了多線程協作的功能:
lockingQueue
除了傳統的 queue 功能(表格左邊的兩列)之外,還提供了阻塞接口 put 和 take,帶超時功能的阻塞接口 offer 和 poll。put 會在隊列滿的時候阻塞,直到有空間時被喚醒;take 在隊 列空的時候阻塞,直到有東西拿的時候才被喚醒。用於生產者-消費者模型尤其好用,堪稱神器。
常見的阻塞隊列有:
- ArrayListBlockingQueue
- LinkedListBlockingQueue
- DelayQueue
- SynchronousQueue
** ConcurrentHashMap**
高效的線程安全哈希 map。請對比 hashTable , concurrentHashMap, HashMap
5.管理類
管理類的概念比較泛,用於管理線程,本身不是多線程的,但提供了一些機制來利用上述的工具做一些封裝。
瞭解到的值得一提的管理類:ThreadPoolExecutor 和 JMX框架下的系統級管理類 ThreadMXBean
ThreadPoolExecutor
如果不瞭解這個類,應該瞭解前面提到的 ExecutorService,開一個自己的線程池非常方便
ExecutorService e = Executors.newCachedThreadPool();
ExecutorService e =Executors.newSingleThreadExecutor();
ExecutorService e = Executors.newFixedThreadPool(3);
// 第一種是可變大小線程池,按照任務數來分配線程,
// 第二種是單線程池,相當於 FixedThreadPool(1)
// 第三種是固定大小線程池。
// 然後運行
e.execute(new MyRunnableImpl());
該類內部是通過 ThreadPoolExecutor 實現的,掌握該類有助於理解線程池的管理,本質上,他們都是 ThreadPoolExecutor 類的各種實現版本。請參見 javadoc:
ThreadPoolExecutor參數解釋
翻譯一下:
corePoolSize: 池內線程初始值與最小值,就算是空閒狀態,也會保持該數量線程。
maximumPoolSize: 線程最大值,線程的增長始終不會超過該值。
keepAliveTime: 當池內線程數高於 corePoolSize 時,經過多少時間多餘的空閒線程才會被回收。回收前處於 wait 狀態
unit:
時間單位,可以使用 TimeUnit 的實例,如 TimeUnit.MILLISECONDS
workQueue: 待入任務(Runnable)的等待場所,該參數主要影響調度策略,如公平與否,是否產生餓死 (starving)
threadFactory: 線程工廠類,有默認實現,如果有自定義的需要則需要自己實現 ThreadFactory 接口並作為參數傳入。
歡迎大家一起交流,喜歡文章記得關注我點贊轉發喲,感謝支持!