12-多執行緒

EUNEIR發表於2024-03-13

程序和執行緒

多執行緒是Java語言的重要特性,大量應用於網路程式設計、伺服器端程式的開發,最常見的UI介面底層原理、作業系統底層原理都大量使用了多執行緒。 我們可以流暢的點選軟體或者遊戲中的各種按鈕,其實,底層就是多執行緒的應用。UI介面的主執行緒繪製介面,如果有一個耗時的操作發生則啟動新的執行緒,完全不影響主執行緒的工作。當這個執行緒工作完畢後,再更新到主介面上。 我們可以上百人、上千人、上萬人同時訪問某個網站,其實,也是基於網站伺服器的多執行緒原理,如果沒有多執行緒,伺服器處理速度會極大降低。 在學習多執行緒之前,我們先要了解幾個關於多執行緒有關的概念。

程式

  • 程式(Program)是一個靜態的概念,一般對應於作業系統中一個可執行檔案,比如:我們要啟動酷狗聽音樂,則對應酷狗的可執行程式。當我們雙擊酷狗,則載入程式到記憶體中,開始執行該程式,於是產生了“程序”。

程序

  • 程序(Process)是一個動態的概念,將程式載入進入記憶體中,則就產生了一個程序。

確切的來說,當一個程式進入記憶體執行,即變成一個程序,程序是處於執行過程中的程式,並且具有一定獨立功能。

現代的作業系統都可以同時啟動多個程序。比如:我們在用酷狗聽音樂、也可以使用idea寫程式碼、也可以同時用瀏覽器檢視網頁。

  • 多程序的意義?

單程序的計算機只能做一件事情,而我們現在的計算機都可以做多件事情。例如,一邊玩遊戲(遊戲程序),一邊聽音樂(音樂程序)。

也就是說現在的計算機都是支援多程序的,可以在一個時間段內執行多個任務。並且,還可以提高CPU的使用率。

執行緒

  • 執行緒是作業系統能夠進行運算排程的最小單位,是一個程序中的執行場景/執行單元;一個程序可以啟動多個執行緒

執行緒是程序中的一個執行單元,負責當前程序中任務的執行,一個程序中至少有一個執行緒,也可以有多個執行緒;一個程式中有多個執行緒在同時執行,我們也稱之為多執行緒程式。

單執行緒程式與多執行緒程式的不同:

  1. 單執行緒程式:若有多個任務只能依次執行。當上一個任務執行結束後,下一個任務才開始執行。
  2. 多執行緒程式:若有多個任務可以同時執行。多個任務可以併發執行,每個執行緒都執行一個任務。

Java程式的執行

對於Java程式來說,在Dos視窗中 輸入java HelloWorld.java後,會先啟動JVM程序

JVM程序會啟動一個主執行緒呼叫main方法,同時在啟動一個垃圾回收器執行緒進行看護,回收垃圾

也就是說當前的Java程式至少有兩個執行緒併發:垃圾回收執行緒和main方法執行的主執行緒

注意:程序A和程序B的棧記憶體獨立不共享

Java中執行緒A和執行緒B的堆記憶體和方法區記憶體共享,棧記憶體獨立一個執行緒一個棧

火車站可以看作是一個程序,火車站中的每一個售票視窗可以看作一個執行緒;不同的人可以在不同的售票視窗購票

多執行緒機制的存在,main方法結束之後只代表主執行緒結束了(主棧空了),其他的執行緒可能還在壓棧彈棧,所有執行緒都結束JVM才會停止執行。

執行緒排程策略

如果多個執行緒被分配到一個CPU核心中執行,則同一時刻只能允許有一個執行緒能獲得CPU的執行權,那麼程序中的多個執行緒就會搶奪CPU的執行權,這就是涉及到執行緒排程策略。

作業系統的執行緒排程策略:

  • 先來先服務 First Come First Serve:作業系統按照作業建立的先後來挑選作業,先建立的作業優先被作業系統執行

容易實現但效率不高。FCFS演算法只考慮了作業的等候時間,而沒有考慮執行時間的長短。也就是說一個晚來但是執行時間很短的作業可能要等待很長時間才可能被投入執行。因此FCFS演算法不利於短作業。

  • 短作業優先 Short Job First:參考運算時間,選取運算時間最短的作業投入執行

容易實現但效率不高。SJF演算法忽視了作業的等待時間,一旦有一個來的早但是很大的作業那麼將長時間得不到排程,容易造成飢餓。

  • 響應比高優先排程演算法

響應比定義:作業的響應時間(等待時間 + 執行時間)與執行時間的比值。

演算法:作業系統計算每個作業的響應比,選擇響應比最高的作業投入執行

  • 優先數排程演算法

根據執行緒優先數,把CPU分配給優先順序最大的執行緒。優先數=靜態優先數+動態優先數

  1. 靜態優先數:執行緒建立時就確定了,在整個執行期間不再改變
  2. 動態優先數:動態優先數線上程執行期間可以改變

靜態優先數的確定:作業系統會基於執行緒所需資源的多少、執行時間的長短、型別(IO/CPU型任務、前臺/後臺執行緒、核心/使用者執行緒)來確定靜態優先數

動態優先數的確定:當執行緒佔用CPU超過一定時常後,其動態優先數就會降低;當進行I/O操作後,就會發生阻塞,此時動態優先數就會升高;當執行緒等待超過一定時長時,動態優先數也會升高

  • 迴圈輪轉排程演算法

把所有就緒執行緒按先進先出的原則排成佇列,新進來的執行緒新增到佇列的末尾。執行緒以時間片q為單位輪流使用CPU。在CPU上執行一個時間片的執行緒被作業系統換下,排到佇列的末尾,等候下一輪執行

2d30ee86510c5f2d587137d295396ba7.png

保證了公平性,讓每個就緒執行緒都有平等機會獲得CPU;保證了互動性,每個執行緒等待(N-1)* q的時間後就可以重新獲得CPU。

時間片的設定:對於時間片q來說,如果q太大就會導致互動差,甚至退化成FCFS演算法;如果q太小,執行緒之間頻繁切換,作業系統的開銷就會增大

為了讓迴圈輪轉的排程演算法更加靈活,每個執行緒的時間片都是可變的,除此之外,還可以組織多個就緒佇列,每個就緒佇列的管理策略都不同,這與先前講過的阻塞佇列是一個道理

分時排程

所有執行緒輪流使用CPU的執行權,並且平均的分配每個執行緒佔用的CPU的時間。也就是迴圈輪轉排程演算法。

搶佔式排程

Java採用的是搶佔式排程,讓優先順序高的執行緒以較大的機率優先獲得CPU的執行權,如果執行緒的優先順序相同,那麼就會隨機選擇一個執行緒獲得CPU的執行權。

併發和並行

併發 concurrency

使用單核CPU的時候,同一時刻只能有一條指令執行,但多個指令被快速的輪換執行,使得在宏觀上具有多個指令同時執行的效果,但在微觀上並不是同時執行的,只是把時間分成若干端,使多個指令快速交替的執行。

圖片3.png

如上圖所示,假設只有一個CPU資源,執行緒之間要競爭得到執行機會。圖中的第一個階段,在A執行的過程中,B、C不會執行,因為這段時間內這個CPU資源被A競爭到了,同理,第二階段只有B在執行,第三階段只有C在執行。其實,併發過程中,A、B、C並不是同時進行的(微觀角度),但又是同時進行的(宏觀角度)。 在同一個時間點上,一個CPU只能支援一個執行緒在執行。因為CPU執行的速度很快,CPU使用搶佔式排程模式在多個執行緒間進行著高速的切換,因此我們看起來的感覺就像是多執行緒一樣,也就是看上去就是在同一時刻執行。

計算機在執行過程中,有很多指令會涉及 I/O 操作,而 I/O 操作又是相當耗時的,速度遠遠低於 CPU,這導致 CPU 經常處於空閒狀態,只能等待 I/O 操作完成後才能繼續執行後面的指令。

為了提高 CPU 利用率,減少等待時間,人們提出了一種 CPU 併發工作的理論。

所謂併發,就是透過一種演算法將 CPU 資源合理地分配給多個任務,當一個任務執行 I/O 操作時,CPU 可以轉而執行其它的任務,等到 I/O 操作完成以後,或者新的任務遇到 I/O 操作時,CPU 再回到原來的任務繼續執行。

1515363219-0.gif

雖然 CPU 在同一時刻只能執行一個任務,但是透過將 CPU 的使用權在恰當的時機分配給不同的任務,使得多個任務在視覺上看起來是一起執行的。CPU 的執行速度極快,多工切換的時間也極短,使用者根本感受不到,所以併發執行看起來才跟真的一樣。

將 CPU 資源合理地分配給多個任務共同使用,有效避免了 CPU 被某個任務長期霸佔的問題,極大地提升了 CPU 資源利用率。

並行 parallellism

併發是針對單核 CPU 提出的,而並行則是針對多核 CPU 提出的。和單核 CPU 不同,多核 CPU 真正實現了“同時執行多個任務”。

多核 CPU 內部整合了多個計算核心(Core),每個核心相當於一個簡單的 CPU,如果不計較細節,你可以認為給計算機安裝了多個獨立的 CPU。

多核 CPU 的每個核心都可以獨立地執行一個任務,而且多個核心之間不會相互干擾。在不同核心上執行的多個任務,是真正地同時執行,這種狀態就叫做並行。

例如,同樣是執行兩個任務,雙核 CPU 的工作狀態如下圖所示:

15153644a-1.gif

雙核 CPU 執行兩個任務時,每個核心各自執行一個任務,和單核 CPU 在兩個任務之間不斷切換相比,它的執行效率更高。

如圖所示,在同一時刻,ABC都是同時執行(微觀、宏觀)。

圖片4.png

併發+並行

在上圖中,執行任務的數量恰好等於 CPU 核心的數量,是一種理想狀態。但是在實際場景中,處於執行狀態的任務是非常多的,尤其是電腦和手機,開機就幾十個任務,而 CPU 往往只有 4 核、8 核或者 16 核,遠低於任務的數量,這個時候就會同時存在併發和並行兩種情況:所有核心都要並行工作,並且每個核心還要併發工作。

例如一個雙核 CPU 要執行四個任務,它的工作狀態如下圖所示:

15153613F-2.gif

每個核心併發執行兩個任務,兩個核心並行的話就能執行四個任務。當然也可以一個核心執行一個任務,另一個核心併發執行三個任務,這跟作業系統的分配方式,以及每個任務的工作狀態有關係。

總結

併發針對單核 CPU 而言,它指的是 CPU 交替執行不同任務的能力;並行針對多核 CPU 而言,它指的是多個核心同時執行多個任務的能力。

單核 CPU 只能併發,無法並行;換句話說,並行只可能發生在多核 CPU 中。

在多核 CPU 中,併發和並行一般都會同時存在,它們都是提高 CPU 處理任務能力的重要手段。

併發程式設計和並行程式設計

  • 在CPU比較繁忙(假設為單核CPU),如果開啟了很多個執行緒,則只能為一個執行緒分配僅有的CPU資源,這些執行緒就會為自己儘量多搶時間片,這就是透過多執行緒實現併發,執行緒之間會競爭CPU資源爭取執行機會。
  • 在CPU資源比較充足的時候,一個程序內的多個執行緒,可以被分配到不同的CPU資源,這就是透過多執行緒實現並行。

至於多執行緒實現的是併發還是並行?上面所說,所寫多執行緒可能被分配到一個CPU核心中執行,也可能被分配到不同CPU執行,分配過程是作業系統所為,不可人為控制。所以,如果有人問我我所寫的多執行緒是併發還是並行的?我會說,都有可能。

總結:單核CPU上的多執行緒,只是由作業系統來完成多工間對CPU的執行切換,並非真正意義上的併發。隨著多核CPU的出現,也就意味著不同的執行緒能被不同的CPU核得到真正意義的並行執行,故而多執行緒技術得到廣泛應用。

不管併發還是並行,都提高了程式對CPU資源的利用率,最大限度地利用CPU資源,而我們使用多執行緒的目的就是為了提高CPU資源的利用率。

分析程式的執行緒

public class ThreadTest01 {
    public static void main(String[] args) {
        System.out.println("main begin");
        m1();
        System.out.println("main over");
    }

    private static void m1() {
        System.out.println("m1 begin");
        m2();
        System.out.println("m1 over");
    }

    private static void m2() {
        System.out.println("m2 begin");
        m3();
        System.out.println("m2 over");
    }

    private static void m3() {
        System.out.println("m3 begin");
        System.out.println("m3 over");
    }
}

目前來說只有1個執行緒,因為程式只有一個棧(不考慮GC執行緒)

執行緒

執行緒的建立方式

  1. 繼承Thread,特點:不能多繼承、無返回值
  2. 實現Runnable,特點:可以繼承其他類,無返回值
  3. 實現Callable,將其交給FutureTask 可以繼承其他類,有返回值

繼承Thread

  • 定義子類繼承 java.lang.Thread 使得子類具備執行緒功能
  • 重寫run方法,封裝執行緒要執行的任務

沒有返回值,不能丟擲更多異常

class MyThread extends Thread{  
    @Override  
    public void run() {  
        //run方法的內容執行在分支執行緒/分支棧中  
        for (int i = 0; i < 100; i++) {  
            System.out.println("分支執行緒 " + this.getName() + " ===> " + i);  
            //Thread類的getName方法可以獲取當前執行緒的名字,子類繼承
        }  
    }  
}
public static void main(String[] args) {  
    Thread myThread = new MyThread();  
  
    //啟動分支執行緒,該行程式碼瞬間結束彈出主棧  
    myThread.start();  
  
    //以後的程式碼還在主棧中執行  
    for (int i = 0; i < 100; i++) {  
        System.out.println("主執行緒 ===> " + i);  
    }  
}

//交替執行
/**
分支執行緒 Thread-0 ===> 0
分支執行緒 Thread-0 ===> 1
主執行緒 ===> 53
分支執行緒 Thread-0 ===> 2
主執行緒 ===> 54
*/
  • myThread.start() 的作用是啟動分支執行緒,在JVM中開闢一個新的棧空間,這段程式碼瞬間結束;只要空間開闢出來了,start()方法就結束了,分支執行緒啟動成功

  • 分支執行緒啟動後,自動呼叫run方法,並且run方法在分支棧的棧底(與主棧的main方法平級)

輸出結果的特點:主執行緒和分支執行緒不一定誰先執行,不一定哪個執行緒執行幾次。

兩個執行緒爭奪CPU執行權,而控制檯只有一個,不一定哪個執行緒搶到執行權向控制檯列印

建立執行緒的思考

執行緒的棧記憶體
  • myThread.start() 與直接呼叫 myThread.run()有何區別?

myThread.run() 不會啟動分支執行緒,不會分配分支棧,等同於還在主棧中執行。run方法的for迴圈不結束,下面的for迴圈就無法進行。

錯誤的 myThread.run() 方式執行:

分支執行緒 ===> 98
分支執行緒 ===> 99
主執行緒 ===> 0
主執行緒 ===> 1

這種方式的記憶體圖:

方法體中的程式碼都是自上而下逐行執行,myThread.start()不結束下面程式碼無法執行,只是 myThread.start()方法瞬間結束run()方法在分支棧中執行(與main中的for同時執行)

在多執行緒中,每個執行緒都有自己獨立的棧記憶體,但是都是共享的同一個堆記憶體。在某個執行緒中程式執行出現了異常,那麼對應執行緒執行終止,但是不影響別的執行緒執行。

而使用 myThread.start() ,啟動分支執行緒並且開闢新的棧空間,這段程式碼完成之後瞬間結束;這段程式碼的任務只是為了開啟一個新的棧空間,只要棧空間開闢出來,start()方法就結束了,執行緒啟動成功。啟動成功的執行緒自動呼叫run()方法,並且run()方法在分支棧的棧底 (與main()方法是平級的)

獲取執行緒的名字

開啟的執行緒都會有自己的獨立執行棧記憶體,那麼這些執行的執行緒的名字是什麼呢?

Thread類有例項方法getName:getName() 返回當前執行緒的名字

  • 構造方法為執行緒起名:
class MyThread extends Thread{  
    public MyThread() {  
    }  
  
    public MyThread(String name) {  
        super(name);  
    }  
  
    @Override  
    public void run() {  
        //run方法的內容執行在分支執行緒/分支棧中  
        for (int i = 0; i < 100; i++) {  
            System.out.println("分支執行緒 " + super.getName() + " ===> " + i);  
        }  
    }  
}
  • Thread的setName方法為執行緒起名

因為繼承Runnable介面無法使用第一種方式設定執行緒名字,只能使用Thread.currentThread().setName()來設定

		// 建立執行緒物件
		Thread th = new Thread();
		// 設定執行緒的名稱
		th.setName("執行緒A");
		// 獲取建立執行緒物件的名稱
		System.out.println("th執行緒物件名稱:" + th.getName());

實現Runnable介面

  • 實現Runnable介面,實現run方法,表示要執行的任務
class MyRunnable implements Runnable{  
  
    @Override  
    public void run() {  
        for (int i = 0; i < 100; i++) {  
            System.out.println("分支執行緒 ===> " + i);  
        }  
    }  
}
Thread thread = new Thread(new MyRunnable());  //Thread的構造方法可接收一個Runnable介面的實現類 
thread.start();  
for (int i = 0; i < 100; i++) {  
    System.out.println("主執行緒 ===> " + i);  
}

可以發現Runnable介面是一個函式式介面:

@FunctionalInterface  
public interface Runnable {  
    void run();  
}

也可以直接使用Lambda表示式:

Thread thread = new Thread(() -> {  
    for (int i = 0; i < 100; i++) {  
        System.out.println("分支執行緒 ===> " + i);  
    }  
});  
thread.start();  
for (int i = 0; i < 100; i++) {  
    System.out.println("主執行緒 ===> " + i);  
}

但是實現Runnable介面就不能使用Thread的例項方法了,可以透過 Thread.currentThread() 獲取當前正在執行的執行緒物件,再獲取執行緒的名字

Thread thread = new Thread(() -> {  
    for (int i = 0; i < 100; i++) {  
        String name = Thread.currentThread().getName();  //獲取當前正在執行的執行緒
        System.out.println("分支執行緒 " + name + " ===> " + i);  
    }  
});

採用這種方式較多,因為Java只支援單繼承,繼承Thread就不能再繼承其他類了

實現Runnable介面的好處

  1. 實現Runnable介面來建立執行緒,實現了任務物件和執行緒物件相分離,實現了程式碼的解耦操作。
  2. 實現Runnable介面來建立執行緒,可以實現資料的共享,而使用Thread建立執行緒不方便資料的共享。

模擬Thread類

透過繼承Thread類建立執行緒,執行緒任務是封裝在Thread子類的run()方法中;透過實現Runnable介面來建立執行緒,執行緒任務是封裝在Runnable介面實現類的run()方法中,那麼呼叫start()方法開啟執行緒,在Thread內部是如何正確的執行執行緒任務的呢?

class ThreadOfMine{  
    private Runnable target;  
  
    public ThreadOfMine(Runnable target) {  
        this.target = target;  
    }  
  
    public ThreadOfMine() {  
          
    }  
      
    public void start(){  
        run();  
    }  
      
    public void run(){  
        if (target != null){  //重點
            target.run();  
        }  
    }  
}

模擬Thread類start方法的實現的核心:

  • 如果建立執行緒採用繼承Thread類的方式,也就是透過Thread子類物件呼叫start()方法,那麼呼叫的就是Thread子類物件的run()方法。
  • 如果建立執行緒採用實現Runnable介面的方式,也就是透過Thread物件呼叫start()方法,那麼呼叫的就是Thread的run()方法,然後再呼叫Runnable介面實現類的run()方法。

實現Callable,交給FutureTask管理

這種方式是對前兩種的補充,前兩種方式無法獲取執行緒的返回值,這種方式可以獲取多執行緒執行的結果。

步驟:

  1. 子類實現Callable介面,重寫call方法,該方法的返回值就是多執行緒執行的結果
//Callable介面:

@FunctionalInterface  
public interface Callable<V> {  
    V call() throws Exception;  
}

實現Callable介面時指定泛型型別,該型別就是多執行緒執行結果的返回值型別

泛型在語義上不支援基本資料型別

  1. 建立FutureTask 未來任務類物件

其中的FutureTask構造方法:

傳遞Callable;建立Thread,傳遞FutureTask

如果傳遞Runnable就是沒有返回值的?

//建立Callable實現類  
Callable<Integer> myCallable = new MyCallable();  
//建立FutureTask,傳遞Callable  
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);  
//建立Thread,傳遞FutureTask  
Thread thread = new Thread(futureTask);  
  
thread.start();  
  
Integer result = futureTask.get();  //會阻塞當前執行緒
System.out.println("futureTask的返回值:" + result); //futureTask的返回值:3

也可以使用Lambda表示式:

//建立FutureTask,傳遞Lambda表示式  
FutureTask<Integer> futureTask = new FutureTask<>(() -> 1 + 2);  
//建立Thread,傳遞FutureTask  
Thread thread = new Thread(futureTask);  
  
thread.start();  
  
Integer result = futureTask.get(); //阻塞當前執行緒  
System.out.println("futureTask的返回值:" + result); //futureTask的返回值:3
  • Callable介面能夠獲得返回值的原因:Callable介面有 V call()方法,這個方法可以獲取V型別的返回值,而這個返回值是在建立Callable介面的實現類時指定的,如果是Lambda表示式就是根據上下文自動型別推斷出的。

  • 而Runnable或繼承Thread時重寫的run方法返回值是void,也就無法獲取到執行緒計算的返回值

  • 繼承Thread或實現Runnable介面重寫的run方法無法丟擲異常,而實現Callable介面重寫call方法可以丟擲異常

但是注意,獲取多執行緒返回值的 futureTask.get() 會阻塞當前執行緒(也就是main執行緒),因為只有futureTask的任務執行完畢才能拿到返回值。

get()方法可能需要很長時間,因為get()方法是為了拿取另一個執行緒的執行結果,另一個執行緒的執行是需要時間的。

執行緒生命週期和狀態控制

執行緒的生命週期

執行緒的生命週期就是執行緒從建立到死亡的過程。

圖片21.png

當執行緒被建立並啟動以後,它既不是一啟動就進入了執行狀態,也不是一直處於執行狀態。

線上程的生命週期中,它要經過新建(New)、就緒(Runnable)、執行(Running)、阻塞(Blocked)和死亡(Dead)五種不同的狀態。尤其是當執行緒啟動以後,它不可能一直“霸佔”著CPU獨自執行,所以CPU需要在多條執行緒之間切換,於是執行緒狀態也會多次在執行、阻塞之間切換。

剛建立出的執行緒處於新建狀態,呼叫start()方法進入就緒狀態;就緒狀態的執行緒又被稱為可執行狀態,表示當前執行緒具有搶奪CPU時間片的權力(執行權)。當一個執行緒搶到CPU時間片後就進入執行狀態run()方法的執行標誌著執行緒進入執行狀態。之前搶奪的CPU時間片用完之後會重新回到就緒狀態搶奪CPU執行權,再次搶奪成功之後會繼續執行run()方法。

主執行緒的時間片使用完畢後,也可能再次繼續搶到時間片。

就緒狀態和執行狀態的來回切換被稱為JVM的排程run() 方法執行結束標誌著執行緒物件進入死亡狀態

當一個執行緒執行run方法的過程中遇到了IO操作(使用者輸入、讀取檔案),執行緒就進入了阻塞狀態,(執行狀態 ---> 阻塞狀態) 進入阻塞狀態的執行緒會放棄搶到的CPU時間片

當阻塞解除之後,會進入就緒狀態繼續搶奪時間片(之前搶到的CPU時間片被釋放掉了)

image.png

其中阻塞狀態還可以細分:

image.png

但是Java裡其實是沒有執行狀態的:

public enum State {  
	NEW,  
	RUNNABLE,  
	BLOCKED,  
	WAITING,  
	TIMED_WAITING,  
	TERMINATED;  
}

當執行緒搶到CPU執行權後,JVM會將執行緒交給作業系統,所以沒有執行狀態

  • 新建狀態 NEW:建立Thread類的例項成功後,則該執行緒物件就處於新建狀態。處於新建狀態的執行緒有自己的記憶體空間,透過呼叫start()方法進入就緒狀態(Runnable)

  • 就緒狀態 RUNNABLE:處於就緒狀態的執行緒已經具備了執行條件(也就是具備了在CPU上執行的資格),但還沒有分配到CPU的執行權,處於“執行緒就緒佇列”,等待系統為其分配CPU。就緒狀態並不是執行狀態,當系統選定一個等待執行的Thread物件後,它就會進入執行狀態。一旦獲得CPU,執行緒就進入執行狀態並自動呼叫run()方法。

  • 執行狀態 RUNNING:處於就緒狀態的執行緒,如果獲得了CPU的排程,就會從就緒狀態變為執行狀態,執行run()方中的任務。 執行狀態的執行緒最為複雜,它可以變為阻塞狀態、就緒狀態和死亡狀態。 如果該執行緒失去了CPU資源,就會又從執行狀態變為就緒狀態,重新等待系統分配資源。也可以對在執行狀態的執行緒呼叫yield()方法,它就會讓出CPU資源,再次變為就緒狀態。

  • 阻塞狀態 BLOCK:在某種特殊的情況下,被人掛起或執行輸入輸出操作時,讓出CPU執行權並臨時中斷自己的執行,從而進入阻塞狀態,直到其進入到就緒狀態,才有機會再次被CPU呼叫以進入到執行狀態。 根據阻塞產生的原因不同,阻塞狀態又可以分為三種:

  1. 等待 WAITING:執行狀態中的執行緒執行wait()方法,使本執行緒進入到等待阻塞狀態。當呼叫notify()或notifyAll()等方法,則該執行緒就會重新轉入就緒狀態。
  2. 阻塞 BLOCKED :執行緒在獲取同步鎖失敗(因為鎖被其它執行緒所佔用),它會進入同步阻塞狀態。當獲取同步鎖成功,則該執行緒就會重新轉入就緒狀態。
  3. 計時等待 TIMED_WATING:透過呼叫執行緒sleep()或join()或發出了I/O請求時,執行緒會進入到計時等待狀態。當sleep()狀態超時、join()等待執行緒終止或者超時、或者I/O處理完畢時,則該執行緒就會重新轉入就緒狀態。
  • 死亡狀態 DEAD:執行緒在run()方法執行完了或者因異常退出了run()方法,該執行緒結束生命週期。此外,如果執行緒執行了interrupt()或stop()方法,那麼它也會以異常退出的方式進入死亡狀態。

執行緒常用方法

static Thread currentThread() // 獲取當前正在執行的執行緒
static void sleep(long time) // 當前執行緒進入休眠,計時等待狀態
static void yield() //出讓執行緒/禮讓執行緒
static void join()  //插入執行緒/插隊執行緒

String getName() //獲取當前執行緒的名字
void setName(String name) //設定當前執行緒的名字
void setPriority() // 設定當前執行緒優先順序
final int getPriority() //獲取當前執行緒優先順序
final void setDaemon(boolean on) //設定守護執行緒

設定/獲取執行緒名字

Thread thread = new Thread(() -> 
						   System.out.println("分支執行緒 " + Thread.currentThread().getName() +  " 執行"));  
System.out.println(thread.getName()); //Thread-0  
thread.setName("BranchThread");  
System.out.println(thread.getName()); //BranchThread  
  
thread.start(); //分支執行緒 BranchThread 執行

可以看到,執行緒的預設名字是Thread-n,再建立一個執行緒物件就變為Thread-1,由Thread類中該方法控制:

static String genThreadName() {  
    return "Thread-" + ThreadNumbering.next();  
}

執行緒的構造方法:

image-20230411153022997

可以看到,在傳遞Runnable時可以指定執行緒的名字

如果是繼承Thread的方式建立執行緒,只能super呼叫父類構造方法

獲取當前執行緒物件

public static Thread currentThread()
public static void main(String[] args) {  
    System.out.println(Thread.currentThread().getName()); //main  
}

如果在分支執行緒中輸出的就是建立分支執行緒時指定的名字或者預設名字

執行緒休眠

public static void sleep(long millis) throws InterruptedException
//讓當前執行緒進入計時等待狀態,放棄佔有的CPU時間片,休眠時間不會完全精確
  • sleep結束就會重新回到就緒狀態搶奪CPU時間片
public static void main(String[] args) throws InterruptedException {   //注意此處丟擲的InterruptedException
    Thread.sleep(5 * 1000); //睡眠5s  
    System.out.println("hello world");  
}
Thread thread = new Thread(() -> {  
    for (int i = 0; i < 10; i++) {  
        System.out.println(Thread.currentThread().getName() + " ---> " + i);  
        try {  
            Thread.sleep(1000);  
        } catch (InterruptedException e) {   
            //此處不能throws,因為父類的方法沒有丟擲異常,子類也不能丟擲編譯時異常。
            e.printStackTrace();  
        }  
    }  
});  
thread.setName("BranchThread");  
thread.start();

sleep可以指定間隔特定的事件執行特定的程式碼

面試題

public class ThreadTest07 {
    public static void main(String[] args) {
        Thread t = new MyThread3();
        t.setName("t");
        t.start();

        try {
            t.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Hello world");
    }
}
class MyThread3 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10_000; i++) {
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }
}
  • try中的t.sleep(1000 * 5) 會讓t執行緒進入休眠狀態嗎?

不會,因為sleep是靜態方法,只能讓當前的執行緒休眠,也就是會讓main執行緒休眠

喚醒休眠狀態

注意:不是中斷執行緒的執行,而是喚醒休眠狀態,讓sleep方法發生異常

對於該類來說:

class SubThread extends Thread{  
    @Override  
    public void run() {  
        System.out.println("run begin");  
        try {  
            Thread.sleep(1000 * 10);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println("run over");  
    }  
}

Thread方法會丟擲InterruptedException物件,在run方法中該物件只能try-catch,不能throws,因為父類的run方法沒有丟擲異常,子類的run方法只能丟擲執行時異常

要求:希望分支執行緒執行5s後醒來

Thread thread = new SubThread();  
thread.start();  
  
Thread.sleep(1000 * 5); //讓主執行緒休眠5s,分支執行緒就能一直執行5s  
  
//干擾分支執行緒  
thread.interrupt();

thread.interrupt() 這種喚醒睡眠的方式依靠的是Java中的異常處理機制:thread呼叫interrupt()方法使得25行thread.sleep(1000 * 10)丟擲異常,被catch語句塊捕捉了

image.png

class SubThread extends Thread{  
    @Override  
    public void run() {  
        System.out.println("run begin");  
        try {  
            Thread.sleep(1000 * 10);  
  
            System.out.println("sleep over");  // <--- 這行程式碼不會執行,在sleep時發生異常直接進入catch語句塊
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println("run over");  
    }  
}

執行緒排程相關方法

執行緒優先順序

  • int getProiority()獲取執行緒優先順序
  • void setProiority() 設定執行緒優先順序 最低優先順序:1 ,預設優先順序:5 ,最高優先順序:10。優先順序高的可能會搶到的CPU時間片的機率更多一些
System.out.println("最高優先順序:" + Thread.MAX_PRIORITY); //10  
System.out.println("預設優先順序:" + Thread.MIN_PRIORITY); //5  
System.out.println("最低優先順序:" + Thread.NORM_PRIORITY); //1  
  
Thread minPriorThread = new Thread(() -> {  
    for (int i = 0; i < 1000; i++) {  
        System.out.println(Thread.currentThread().getName() + " ---> " + i);  
    }  
},"minPriorThread");  
  
Thread maxPriorThread = new Thread(() -> {  
    for (int i = 0; i < 1000; i++) {  
        System.out.println(Thread.currentThread().getName() + " ---> " + i);  
    }  
},"maxPriorThread");  
  
minPriorThread.setPriority(Thread.MIN_PRIORITY);  
maxPriorThread.setPriority(Thread.MAX_PRIORITY);

多次觀察可以發現,低優先順序的執行緒總是最後執行完畢

合併執行緒

void join() :讓呼叫者插入,當前的執行緒停止執行,進入阻塞狀態,直到呼叫者執行緒執行完畢當前執行緒才能繼續執行

將呼叫者執行緒合併入當前執行緒中,多執行緒合併為單執行緒

Thread thread = new Thread(() -> {  
    try {  
        System.out.println("Branch Thread begin");  
        Thread.sleep(1000 * 2);  
        System.out.println("Branch Thread over");  
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    }  
},"branchThread");  
  
thread.start();  

System.out.println("main begin");

for (int i = 0; i < 1000; i++) {  
    System.out.println(Thread.currentThread().getName() + " ---> " + i);  
}  
System.out.println("main over");

多次測試的結果如下:

main begin
Branch Thread begin
main ---> 0
main ---> 1
...
main ---> 998
main ---> 999
main over
Branch Thread over

分支執行緒和主執行緒同步執行,如果要讓分支執行緒先執行,就使用join方法讓分支執行緒插隊:

Thread thread = new Thread(() -> {  
    try {  
        System.out.println("Branch Thread begin");  
        Thread.sleep(1000 * 2);  
        System.out.println("Branch Thread over");  
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    }  
},"branchThread");  
  
thread.start();  
  
System.out.println("main begin");  
  
	try {  
	    thread.join(); //分支執行緒插隊  
	} catch (InterruptedException e) {  
	    e.printStackTrace();  
	}  
  
for (int i = 0; i < 1000; i++) {  
    System.out.println(Thread.currentThread().getName() + " ---> " + i);  
}  
System.out.println("main over");

thread.join() 主執行緒進入阻塞,thread執行緒會先執行,執行完畢主執行緒再繼續執行

main begin
Branch Thread begin
Branch Thread over
main ---> 0
main ---> 1
...
main ---> 998
main ---> 999
main over

該方法還有過載的方法:

image.png

出讓執行緒

static void yield() :讓當前執行緒放棄CPU時間片,回到就緒佇列

並不是阻塞當前執行緒,只是放棄CPU時間片回到就緒佇列和其他執行緒一起搶奪執行權,當前執行緒也可能立刻再次搶奪到CPU執行權

Thread thread = new Thread(() -> {  
    for (int i = 0; i < 10000; i++) {  
        if (i % 100 == 0){   //每100次迴圈讓出1次
            Thread.yield();  
        }  
        System.out.println(Thread.currentThread().getName() + " ---> " + i);  
    }  
},"BranchThread");  
  
thread.start();  
  
for (int i = 0; i < 10000; i++) {  
    if (i % 100 == 0){  
    }  
    System.out.println(Thread.currentThread().getName() + " ---> " + i);  
}

執行結果:

BranchThread ---> 0  //讓出,緊接著又搶到執行權
BranchThread ---> 1
main ---> 0

BranchThread ---> 100 //讓出,main搶到執行權
main ---> 92

BranchThread ---> 200 //讓出,緊接著又搶到執行權
BranchThread ---> 201

該方法使各執行緒獲得CPU時間片的機率可能均勻?

終止執行緒執行

強制終止

stop() :強制終止呼叫者執行緒執行,可能會導致資料丟失,已過時

Thread thread = new Thread(() -> {  
    for (int i = 0; i < 10; i++) {  
        System.out.println(Thread.currentThread().getName() + "正在執行 ---> " + i);  
        try {  
            Thread.sleep(1000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  
},"BranchThread");  
  
thread.start();  
  
//讓分支執行緒執行5s後強制停止  
Thread.sleep(1000 * 5);  
thread.stop();
合理終止

在子執行緒中自行判斷是否需要終止,如果需要終止就停止當前執行緒的執行,並在終止之前進行儲存資料的操作

class BranchThread extends Thread{  
    boolean run = true;  
  
    public BranchThread(String name) {  
        super(name);  
    }  
  
    public BranchThread() {  
    }  
  
    @Override  
    public void run() {  
        for (int i = 0; i < 10; i++) {  
            if (run){  
                System.out.println(super.getName() + " ---> " + i);  
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }else {  
                System.out.println("儲存資料");  
                return;  
            }  
        }  
    }  
}
BranchThread thread = new BranchThread("BranchThread");  
thread.start();  
  
//讓分支執行緒執行5s  
Thread.sleep(1000 * 5);  
//告知子執行緒應該暫停  
thread.run = false;

這樣分支執行緒在執行的過程中每次都會判斷是否應該執行。

守護執行緒

守護執行緒可以看作後臺執行緒,Java語言中分為:使用者執行緒(main也是使用者執行緒)和守護執行緒(例如垃圾回收執行緒)

守護執行緒是一個死迴圈所有的非守護執行緒一旦結束,守護執行緒自動陸續結束

示例:

  1. 聊天是執行緒0,同時傳輸檔案是執行緒1;當聊天結束,傳輸檔案隨之結束,就可以將傳輸檔案設定為守護執行緒
  2. 每天0時,系統資料自動備份;需要使用定時器,可以將定時器設定為守護執行緒。

模擬系統每擱1s備份一次輸出,系統結束時備份結束

class BackCopyDataThread extends Thread{  
    public BackCopyDataThread() {  
    }  
  
    public BackCopyDataThread(String name) {  
        super(name);  
    }  
  
    @Override  
    public void run() {  
        int count = 0;  
        while (true){  
            System.out.println(Thread.currentThread().getName() + " 備份了:" + (++count) + "次");  
            try {  
                Thread.sleep(1000);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
}
Thread backCopyDataThread = new BackCopyDataThread("BackCopyDataThread");  
  
//主執行緒啟動時備份執行緒啟動  
backCopyDataThread.start();  
  
for (int i = 0; i < 5; i++) {  
    System.out.println("使用者執行操作");  
    Thread.sleep(1000);  
}

主執行緒結束(for迴圈結束),資料備份執行緒也應該結束,此時就可以將資料備份執行緒設定為守護執行緒:

public static void main(String[] args) throws InterruptedException {  
    Thread backCopyDataThread = new BackCopyDataThread("BackCopyDataThread");  

    //設定為守護執行緒
    backCopyDataThread.setDaemon(true);  
    
    //主執行緒啟動時守護執行緒啟動  
    backCopyDataThread.start();  
  
    for (int i = 0; i < 5; i++) {  
        System.out.println("使用者執行操作");  
        Thread.sleep(1000);  
    }  
}

守護執行緒中即使是死迴圈也會在使用者執行緒結束後自動結束

  • setDaemon(true)方法必須在啟動執行緒前呼叫,否則丟擲IllegalThreadStateException異常。

執行緒同步

學習了執行緒的建立和狀態控制,但是每個執行緒之間幾乎都沒有什麼太大的聯絡。但是可能存在多個執行緒對同一個資料進行操作,這個共享資料就會出現各種問題,由於同一程序的多個執行緒共享同一個資料來源,在帶來方便的同時,也帶來了訪問衝突這個嚴重的問題。Java語言提供了專門機制以解決這種衝突,有效避免了同一個資料物件被多個執行緒同時訪問。

資料安全

在開發中專案都是執行在伺服器當中的,伺服器已經將執行緒的定義、執行緒物件的建立、執行緒的啟動等已經全部實現了,這些程式碼我們都不需要編寫。需要關注的是資料在多執行緒併發的環境下是否是安全的。

資料不安全的情況:

  • 多執行緒併發
  • 多執行緒共享一個資料
  • 多執行緒對共享資料進行了修改

滿足這三個條件就會出現執行緒安全問題。解決的方法是:執行緒排隊執行,也就是不能併發執行,這種機制被稱為執行緒同步機制,執行緒同步機制會犧牲一定的執行效率(資料安全是最重要的)

  • 同步程式設計模型(排隊):執行緒t1和執行緒t2在執行時必須等待其中一個執行緒執行結束再執行另一執行緒;兩個執行緒之間發生了等待關係。
  • 非同步程式設計模型(併發):執行緒t1和執行緒t2各自執行,t1不考慮t2,t2也不考慮t1;就是 多執行緒併發 。

執行緒安全的經典問題:取錢案例

銀行賬戶中有5000元,兩個人同時操作這一個賬戶,都要取出3000元。

public class Account {  
    private String actno;  
    private double balance;  
  
    public Account() {  
    }  
  
    public Account(String actno, double balance) {  
        this.actno = actno;  
        this.balance = balance;  
    }  

	public void draw(double money){  
	    if (this.balance >= 3000){  
	        double newBalance = this.balance - money;  
	        this.setBalance(newBalance);  
	        System.out.println(actno + "取款:" + money + " 成功");  
	        System.out.println("剩餘:" + balance);  
	    }else {  
	        System.out.println("餘額不足,剩餘:" + balance);  
	    }  
	}
	
    public double getBalance() {  
        return balance;  
    }  
  
    public void setBalance(double balance) {  
        this.balance = balance;  
    }  
}

兩個獨立的分支執行緒都使用了堆記憶體的共享物件

public class AccountThread implements Runnable{  
  
    private Account account;  
  
    public AccountThread(Account account) {  
        this.account = account;  
    }  
  
    @Override  
    public void run() {  
        account.draw(3000);  
    }  
}

同時取錢:

Account account = new Account("act-001", 5000.0);  
Thread thread1 = new Thread(new AccountThread(account));  
Thread thread2 = new Thread(new AccountThread(account));  
  
thread1.start();  
thread2.start();

在draw方法中

	public void draw(double money){  
	    if (this.balance >= 3000){  
	        double newBalance = this.balance - money;  
	        this.setBalance(newBalance);  
	        System.out.println(actno + "取款:" + money + " 成功");  
	        System.out.println("剩餘:" + balance);  
	    }else {  
	        System.out.println("餘額不足,剩餘:" + balance);  
	    }  
	}

this.setBalance(newBalance) 執行之前,餘額都是不會更新的,假設執行緒A判斷餘額 >= 3000,此時執行緒B也搶到了執行權,也判斷了餘額 >= 3000,這樣兩個執行緒都會取到錢,並且餘額可能是:

  • 2000:執行緒A計算完畢newBalance = 2000後執行權被執行緒B搶走,執行緒B計算完newBalance = 2000後A繼續執行,這樣兩個執行緒都是setBalance(2000),餘額就是2000
  • -1000:執行緒A判斷為true後執行權被B搶走,執行緒B完整的執行流程並且setBalance(2000),此時執行緒A計算的新餘額就是newBalance = 2000 - 3000

模擬這個效果:在判斷完畢後執行緒都休眠1s:

public void draw(double money){  
    if (this.balance >= 3000){  
        double newBalance = this.balance - money;  
  
        try {  
            Thread.sleep(1000); // 大機率為2000,  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
          
        this.setBalance(newBalance);  
  
        System.out.println(actno + "取款:" + money + " 成功");  
        System.out.println("剩餘:" + balance);  
    }else {  
        System.out.println("餘額不足,剩餘:" + balance);  
    }  
}

執行結果:

act-001取款:3000.0 成功
act-001取款:3000.0 成功
剩餘:2000.0
剩餘:2000.0

為了避免這樣的事情發生,我們要保證執行緒同步互斥,所謂同步互斥就是:併發執行的多個執行緒在某個時間內只允許一個執行緒在執行並訪問共享資料。線上程A進行取款的時候執行緒B一定要排隊等待A取款完成才能進行取款。

解決方法:使用同步程式碼塊synchronized,也就是執行緒同步機制

同步程式碼塊synchronized

需要保證以下的核心程式碼不能被兩個執行緒併發執行:

public void withDraw(double money){

        double before = this.getBalance();

        double after = before - money;

        this.setBalance(after);
}

也就是執行緒必須完整的執行該方法,一個執行緒執行結束另一個執行緒才能繼續執行。

執行緒同步語法:

synchronized(){ //小括號中的資料必須是多執行緒共享的資料 才能達到多執行緒排隊。
    //執行緒同步程式碼塊
    //多個執行緒訪問這個程式碼塊時,受到同步的執行緒必須排隊執行
}

synchronized()的小括號中寫哪個資料取決於想讓哪些執行緒同步。假設當前有t1、t2、t3、t4、t5,只想讓t1、t2、t3排隊,而t4、t5不必排隊,小括號中就寫t1、t2、t3共享的物件,而這個物件對於t4、t5來說是不共享的。

對於本例來說,共享的資料就是兩個執行緒都持有的Account物件,也即是Account中draw方法的呼叫者this:

public void draw(double money){  
    synchronized (this){  //進行執行緒同步
        if (this.balance >= 3000){  
            double newBalance = this.balance - money;  
  
            try {  
                Thread.sleep(1000); // 大機率為2000,  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
  
            this.setBalance(newBalance);  
  
            System.out.println(actno + "取款:" + money + " 成功");  
            System.out.println("剩餘:" + balance);  
        }else {  
            System.out.println("餘額不足,剩餘:" + balance);  
        }  
    }  
}

每個Java物件都對應一把鎖,A執行緒遇到synchronized關鍵字後就會尋找小括號中的物件的物件鎖,找到之後佔有該鎖並執行同步程式碼塊的內容。直到同步程式碼塊執行結束才會釋放該物件鎖。假設執行緒A佔有該鎖,執行緒B遇到synchronized後尋找該鎖,發現該鎖在鎖池 LockPool中不存在,執行緒B就只能進入阻塞狀態等待執行緒A歸還該物件鎖。

注意:共享物件的選擇很重要,共享物件一定是需要排隊執行的執行緒所共享的

處於執行狀態的執行緒遇到synchronized關鍵字後會放棄當前佔有的CPU時間片,進入鎖池LockPool中尋找共享物件的物件鎖,沒有找到會進入阻塞狀態等待,找到後重新回到就緒佇列搶奪CPU時間片

synchronized指定的是同步監視器

  • 同步例項變數:

假設Account類多了一個屬性:

public class Account :
    private String actno;
    private double balance;

    private Object obj = new Object(); //例項變數

對該屬性進行同步也是可以的:

public void withDraw(double money){
    synchronized (obj){ //可以傳遞例項變數
        double before = this.getBalance();
        double after = before - money;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.setBalance(after);
    }
}

同一個物件的例項變數都只有一份,執行緒A佔用了obj的物件鎖之後執行緒B只能等待執行緒A釋放鎖

  • 不能同步方法區域性變數:
public void withDraw(double money){
	//區域性變數
	Object obj2 = new Object();
    synchronized (obj2){
        double before = this.getBalance();
        double after = before - money;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.setBalance(after);
    }
}

方法體中的變數是區域性變數,而兩個執行緒對應了兩個棧,區域性變數就有兩份

  • 對空引用的同步:
Object kongyinyong = null;
synchronized (kongyinyong){
    double before = this.getBalance();
    double after = before - money;
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    this.setBalance(after);
    /*
    Exception in thread "t1" Exception in thread "t2" java.lang.NullPointerException: Cannot enter synchronized block because "this.obj" is null
	at ThreadSafe.Account.withDraw(Account.java:46)
	at ThreadSafe.AccountThread.run(AccountThread.java:15)
java.lang.NullPointerException: Cannot enter synchronized block because "this.obj" is null
	at ThreadSafe.Account.withDraw(Account.java:46)
	at ThreadSafe.AccountThread.run(AccountThread.java:15)
    */

會導致空指標異常。

  • 其他內容的同步:
synchronized ("abc"){
    double before = this.getBalance();
    double after = before - money;
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    this.setBalance(after);
}

對字串字面量"abc"進行同步,這樣也是可以完成功能的,因為字串物件在常量池中也只有一份,但這樣做就導致所有執行緒執行draw方法都需要同步進行,而不只是持有了共享Account物件的執行緒

  • 對類檔案物件同步
public void draw(double money){  
    synchronized (Account.class){  // 對類檔案物件同步
        if (this.balance >= 3000){  
            double newBalance = this.balance - money;  
  
            try {  
                Thread.sleep(1000);   
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
  
            this.setBalance(newBalance);  
  
            System.out.println(actno + "取款:" + money + " 成功");  
            System.out.println("剩餘:" + balance);  
        }else {  
            System.out.println("餘額不足,剩餘:" + balance);  
        }  
    }  
}

這樣做會導致所有執行本方法的執行緒都需要同步,而不是共享Account物件的執行緒同步。

對於以上兩種同步方式,假設有其他賬戶:

        //建立賬戶物件:
        Account act = new Account("act-001",10000);
        Account anotherAct = new Account("act-002",10000);

        //兩個執行緒共享同一個賬戶
        AccountThread t1 = new AccountThread(act);
        AccountThread t2 = new AccountThread(act);

        AccountThread t3 = new AccountThread(anotherAct);

對字面量"abc"或類檔案物件的同步,會導致t1、t2、t3同時同步,而不僅僅是持有共享物件的t1、t2同步

擴大同步範圍

同步程式碼塊越小,效率越高

如果不對Account類的draw方法加鎖,對執行緒的run方法中呼叫處進行加鎖:

@Override  
public void run() {  
    synchronized (account) {  
        account.draw(3000);  
    }  
}

這樣也是可以保證安全的,但是這種方式擴大了執行緒同步範圍,效率變低

  • 此處不能對this加鎖,因為Runnable的實現類會被建立兩次
  • 此處可以對位元組碼檔案物件加鎖,雖然實現類被建立了兩次,但是位元組碼檔案物件只有一個

例項方法的synchronized關鍵字

public synchronized void withDraw(double money)
  • synchronized出現在例項方法上一定鎖的是當前方法的呼叫者,也是this
  • synchronized出現在靜態方法上鎖的是當前類的位元組碼檔案物件

在同步方法中只能透過this呼叫三個方法

這樣不夠靈活,同步的是整個方法體,可能會無故擴大同步範圍,導致程式執行效率變低。

對於StringBuffer類:

@Override  
public synchronized int compareTo(StringBuffer another) {  
    return super.compareTo(another);  
}  
  
@Override  
public synchronized int length() {  
    return count;  
}  
  
@Override  
public synchronized int capacity() {  
    return super.capacity();  
}

StringBuffer執行緒安全的,而StringBuilder是非執行緒安全的;所以使用時建議作為區域性變數用StringBuilder(不需要去lockpool)

  • ArrayList是非執行緒安全的
  • Vector是執行緒安全的
  • HashMap HashSet是非執行緒安全的
  • HashTable是執行緒安全的

總結

只有共享資源的讀寫訪問才需要同步,如果不是共享資源,根本沒有同步的必要

  • 第一種:同步程式碼塊
synchronized(執行緒共享物件){
    同步程式碼塊;
}
  • 第二種:例項方法
public synchronized void doSome(){ 鎖的是方法呼叫者,也就是this
    同步範圍為整個方法體;
}

同步例項方法的可讀性更好,但是效率略低

  • 第三種:靜態方法 表示類鎖不管建立多少個物件,永遠只有一把類鎖,鎖的是當前類的位元組碼檔案
public static synchronized void doSome(){  鎖的是當前類的位元組碼物件
  
}

類鎖是保證靜態變數的執行緒安全。因為靜態方法能夠訪問的就是靜態變數。

synchronized 加鎖時判斷括號內的物件是否是共享的,並且儘可能使用小範圍的鎖

  • 鎖的選擇儘量保證是多執行緒共享的物件,如果隨意選擇一個唯一物件會影響其他無關執行緒的執行。

什麼時候會釋放鎖?

  1. 獲取鎖的執行緒執行完了同步程式碼,然後執行緒釋放對鎖的佔有。
  2. 執行緒執行發生了異常或錯誤,此時虛擬機器(JVM)會讓執行緒自動釋放鎖。
  3. 當執行緒執行同步方法或同步程式碼塊時,程式執行了同步鎖物件的wait()方法。

synchronized是可重入鎖

在使用synchronized時,當一個執行緒得到一個物件鎖後(只要該執行緒還沒有釋放這個物件鎖),再次請求此物件鎖時是可以再次得到該物件的鎖的。

可重入鎖也支援在父子類繼承的環境中。當存在父子類繼承關係時,子類是完全可以透過“可重入鎖”呼叫父類的同步方法。

  • 在同步方法中,可以呼叫當前類的另一個同步方法:
class TestRunnable implements Runnable {
	@Override
	public void run() {
		method01();
	}
	public synchronized void method01() {
		System.out.println("執行method01方法啦");
		// 在同步方法中呼叫同一個物件的另外一個同步方法 
		method02();
	}
	public synchronized void method02() {
		System.out.println("執行method02方法啦");
	}
}
// 測試類
public class Test {
	public static void main(String[] args) {
		new Thread(new TestRunnable()).start();
	}
}
  • 在子類同步方法中,可以呼叫父類的同步方法
// 父類
class Parent {
	public synchronized void parentMethod() {
		System.out.println("父類parentMethod方法被執行啦");
	}
}
// 子類
class ChildRunnable extends Parent implements Runnable {
	@Override
	public void run() {
		childMethod();
	}
	public synchronized void childMethod() {
		System.out.println("子類childMethod方法被執行啦");
		// 呼叫父類方法
		super.parentMethod();
	}
}
// 測試類
public class Test {
	public static void main(String[] args) {
		new Thread(new ChildRunnable()).start();
	}
}

同步方法的重寫

  • 子類重寫父類的同步方法,子類重寫的方法可以為非同步方法
//父類
class Parent {
	// 父類的method方法為同步方法
	public synchronized void method() {
		System.out.println("父類method方法");
	}
}
//子類
class Child {
	// 子類重寫父類的method方法,但是子類方法可以不是同步方法
	public void method() {
		System.out.println("子類method方法");
	}
}

面試題

  • 題1
package ThreadExam;

public class Exam01 {
    public static void main(String[] args) throws Exception {
        MyClass mc = new MyClass();
        Thread t1 = new MyThread(mc);
        Thread t2 = new MyThread(mc);
        t1.setName("t1");
        t2.setName("t2");

        t1.start();
        Thread.sleep(1000);//保證t1先執行
        t2.start();
    }
}
class MyThread extends Thread{

    private MyClass mc;

    public MyThread(MyClass mc) {
        this.mc = mc;
    }

    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("t1")){
            mc.doSome();
        }
        if (Thread.currentThread().getName().equals("t2")){
            mc.doOther();
        }
    }
}
class MyClass{
    public synchronized void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000 * 10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }
    public void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}

問:doOther()方法的執行需不需要等doSome()方法的結束?

不需要,因為doOther方法沒有加鎖,doSome方法鎖的是this,也就是共享物件mc,如果doOther方法也加鎖了就需要等待doSome方法的結束。

  • 題2

在上題的基礎上,變為:

class MyClass{
    public synchronized void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000 * 10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }
    public synchronized void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}

問:doOther()方法的執行需不需要等doSome()方法的結束?

需要,此時doSome方法的執行(執行緒t2)需要尋找共享物件鎖

  • 題3

在上題的基礎上,變為:

public static void main(String[] args) throws Exception {
    MyClass mc1 = new MyClass();
    MyClass mc2 = new MyClass();
    Thread t1 = new MyThread(mc1);
    Thread t2 = new MyThread(mc2);
    t1.setName("t1");
    t2.setName("t2");

    t1.start();
    Thread.sleep(1000);//保證t1先執行
    t2.start();

問:doOther()方法的執行需不需要等doSome()方法的結束?

不需要,鎖的是不同的mc物件

  • 題4
class MyClass{
    public static synchronized void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000 * 10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }
    public synchronized void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}

public static void main(String[] args) throws Exception {
    MyClass mc1 = new MyClass();
    MyClass mc2 = new MyClass();
    Thread t1 = new MyThread(mc1);
    Thread t2 = new MyThread(mc2);
    t1.setName("t1");
    t2.setName("t2");

    t1.start();
    Thread.sleep(1000);//保證t1先執行
    t2.start();
}

問:doOther方法需不需要等待doSome方法執行結束?

不需要,此時一個鎖的是類,一個鎖的是物件

  • 題5
class MyClass{
    public static synchronized void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000 * 10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }
    public static synchronized void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}

public static void main(String[] args) throws Exception {
    MyClass mc1 = new MyClass();
    MyClass mc2 = new MyClass();
    Thread t1 = new MyThread(mc1);
    Thread t2 = new MyThread(mc2);
    t1.setName("t1");
    t2.setName("t2");

    t1.start();
    Thread.sleep(1000);//保證t1先執行
    t2.start();
}

問:doOther方法需不需要等待doSome方法執行結束?

需要,類物件只有一個。

死鎖

導致死鎖的根源在於不適當地運用“synchronized”關鍵詞來管理執行緒對特定物件的訪問。比如有兩個物件A 和 B 。第一個執行緒鎖住了A,然後休眠1秒,輪到第二個執行緒執行,第二個執行緒鎖住了B,然後也休眠1秒,然後有輪到第一個執行緒執行。第一個執行緒又企圖鎖住B,可是B已經被第二個執行緒鎖定了,所以第一個執行緒進入阻塞狀態,又切換到第二個執行緒執行。第二個執行緒又企圖鎖住A,可是A已經被第一個執行緒鎖定了,所以第二個執行緒也進入阻塞狀態。就這樣,死鎖造成了。

public class DeadLockTest01 {  
    public static void main(String[] args) {  
        Object o1 = new Object();  
        Object o2 = new Object();  
        Thread thread01 = new BranchThread01(o1, o2);  
        Thread thread02 = new BranchThread02(o1, o2);  
  
        thread01.start();  
        thread02.start();  
    }  
}  
  
class BranchThread01 extends Thread{  
    private Object o1;  
    private Object o2;  
  
    public BranchThread01(Object o1, Object o2) {  
        this.o1 = o1;  
        this.o2 = o2;  
    }  
  
    @Override  
    public void run() {  
        synchronized (o1){  
            try {  
                sleep(100); //保證兩個執行緒對不同物件加鎖成功  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
  
            synchronized (o2){  
                System.out.println("BranchThread01");  
            }  
        }  
    }  
}  
class BranchThread02 extends Thread{  
    private Object o1;  
    private Object o2;  
  
    public BranchThread02(Object o1, Object o2) {  
        this.o1 = o1;  
        this.o2 = o2;  
    }  
  
    @Override  
    public void run() {  
        synchronized (o2){  
            try {  
                sleep(100); //保證兩個執行緒對不同物件加鎖成功  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
  
            synchronized (o1){  
                System.out.println("BranchThread02");  
            }  
        }  
    }  
}

注意:synchronized在開發中最好不要巢狀使用

開發中解決資料安全問題

並非一上來就選擇synchronized,這會讓執行效率降低,進而導致系統的使用者併發量降低;在不得已的情況下再使用synchronized

  • 儘量使用區域性變數代替 靜態變數 和 例項變數
  • 如果必須是例項變數,可以考慮建立多個物件,這樣例項變數的記憶體就不共享了(一個執行緒對應一個物件)
  • 如果不能使用區域性變數,物件也不能建立多個,只能使用synchronized

練習

  • 練習一
本案例模擬一個簡單的銀行系統,使用兩個不同的執行緒向同一個賬戶存錢。
賬戶的初始餘額是1000元,兩個執行緒每次儲存100元,分別各儲存1000元,不允許出現錯誤資料。
程式執行結果如下圖所示:不要求輪流存
public class Result {  
    public static void main(String[] args) {  
        Account account = new Account(0);  
        Thread zhangsan = new Thread(new SaveMoneyThread(account), "zhangsan");  
        Thread lisi = new Thread(new SaveMoneyThread(account), "lisi");  
  
        zhangsan.start();  
        lisi.start();  
  
    }  
}  
class SaveMoneyThread implements Runnable{  
  
    private Account account;  
      
    public SaveMoneyThread(Account account) {  
        this.account = account;  
    }  
  
    @Override  
    public void run() {  
        for (int i = 0; i < 1000; i++) {  
            account.save(100);  
        }  
    }  
  
}  
class Account{  
    private Integer balance;  
      
    public Account(Integer balance) {  
        this.balance = balance;  
    }  
  
    /**  
     * 獲取  
     * @return money  
     */    public Integer getBalance() {  
        return balance;  
    }  
  
    /**  
     * 設定  
     * @param balance  
     */  
    public void setBalance(Integer balance) {  
        this.balance = balance;  
    }  
  
    public synchronized void save(Integer money){   //同步
        setBalance(getBalance() + 100);  
        System.out.println(Thread.currentThread().getName() 
											        +" 存入了 " + money + " 元,餘額:" + getBalance());  
    }  
}
  • 練習二
小明上課時打瞌睡,被老師發現,老師懲罰他抄寫100遍單詞"HelloWorld",而且老師每發現一個同學,懲罰的次數和抄寫的內容都不一樣。恰好今天學習多執行緒,於是乎小明就找到了小王幫助他一起抄寫單詞。
請使用多執行緒模擬小明和小王一起完成抄單詞的懲罰。
程式執行效果如下圖:不要求輪流寫,不要求平均分配抄寫次數
public class Result {  
    public static void main(String[] args) {  
        //定義一個WordThread即可  
        WordThread task = new WordThread();  
        new Thread(task,"小明").start();  
        new Thread(task,"小王").start();  
    }  
}  
class WordThread implements Runnable{  
  
    private static Integer COUNT = 100;  
      
    public WordThread() {  
  
    }  
  
    @Override  
    public void run() {  
        int count = 0;  
        while (true){  
            synchronized (WordThread.class){  
                if (COUNT > 0) {  
                    --COUNT;  
                    System.out.println(Thread.currentThread().getName() 
						                    + "抄寫了一次,兩人還需抄寫 " + (--COUNT) + " 次");  
                    count++;  
                }else {  
                    break;  
                }  
            }  
  
        }  
  
        System.out.println(Thread.currentThread().getName() + " 抄了:" + count);  
    }  
}
  • 練習三
某房產公司大促銷,所有購房者可以參加一次抽獎,抽獎箱中總共有10個獎品,
分別是:"蘋果手機","華為手機","三洋踏板摩托","杜拜7日遊","蘋果筆記本",
"聯想筆記本","小米空氣清淨機","格力空調","海爾冰箱","海信電視"
所有抽獎者分成兩組進行抽獎,請建立兩個執行緒,名稱分別為“第一組”和“第二組”,隨機從抽獎箱中完成抽獎
程式執行效果如下圖:不要求輪流寫,不要求平均分配抽獎次數
public class Result {  
    public static void main(String[] args) {  
        ArrayList<String> list = new ArrayList<>();  
        Collections.addAll(list,"蘋果手機","華為手機","三洋踏板摩托","杜拜7日遊","蘋果筆記本","聯想筆記本","小米空氣清淨機","格力空調","海爾冰箱","海信電視");  
        Runnable getPrice = new GetPrice(list);  
        Thread firstGroup = new Thread(getPrice, "第一組");  
        Thread secondGroup = new Thread(getPrice, "第二組");  
        secondGroup.start();  
        firstGroup.start();  
    }  
}  
class GetPrice implements Runnable{  
  
    private List<String> list;  
  
    public GetPrice(List<String> list) {  
        this.list = list;  
    }  
  
    @Override  
    public void run() {  
        while (true){  
            synchronized (list){  
                if (list.isEmpty()){  
                    return;  
                }else {  
                    Collections.shuffle(list);  
                    String price = list.removeLast();  
                    System.out.println(Thread.currentThread().getName() + " 抽出了:" + price);  
                }  
            }  
        }  
    }  
}
  • 練習四
某公司組織年會,會議入場時有兩個入口,在入場時每位員工都能獲取一張雙色球彩票,假設公司有100個員工,利用多執行緒模擬年會入場過程,並分別統計每個入口入場的人數,以及每個員工拿到的彩票的號碼。
雙色球球規則:
雙色球: 由6個紅色球號碼和1個藍色球號碼組成。
紅色球: 從1--33中選擇。
藍色球: 從1--16中選擇。
紅球從小到大的順序,不可重複,藍球和紅球可以重複
執行緒執行後列印格式如下:不要求兩個入口輪流進,不要求平均分配進入人數
public class Result {  
    public static void main(String[] args) {  
        Entrance entrance = new Entrance();  
        Thread thread1 = new Thread(entrance, "入口1");  
        Thread thread2 = new Thread(entrance, "入口2");  
  
        thread2.start();  
        thread1.start();  
    }  
}  
  
class Entrance implements Runnable {  
  
    private static int TOTAL = 10000;  
  
    @Override  
    public void run() {  
        int count = 0;  
  
        while (true) {  
            synchronized (this) {  
                if (TOTAL == 0) {  
                    break;  
                }  
                String number = getRandomNumber();  
                System.out.println(Thread.currentThread().getName() 
								                + " 進入了 " + (TOTAL--) + " 號員工,號碼是:" + number);  
                count++;  
            }  
        }  
  
        System.out.println(Thread.currentThread().getName() + " 進入了 " + count + " 名員工");  
    }  
  
    private String getRandomNumber() {  
        Random random = new Random();  
        TreeSet<Integer> integers = new TreeSet<>();  
        while (integers.size() != 6) {  
            integers.add(random.nextInt(33) + 1);  
        }  
        return "紅球:" + integers + " 藍球:" + (random.nextInt(16) + 1);  
    }  
}
  • 練習五
編寫四個執行緒兩個執行緒列印1-52的整數,另兩個執行緒打字母印A-Z.
整體列印數字和字母的順序沒有要求,要求分別單獨看數字,單獨看字母為升序排列的
每個數字和字母之間用空格隔開
不要求兩個執行緒輪流打
public class Result {  
    public static void main(String[] args) {  
        Runnable runnableNumber = () -> {  
            for (int i = 1; i < 53; i++) {  
                synchronized ("Number"){ //lambda不能鎖this  
                    System.out.print(i + " ");  
                }  
            }  
        };  
        Runnable runnableChar = () -> {  
            for (int i = 0; i < 26; i++) {  
                synchronized ("Char"){  
                    System.out.print((char) ('A' + i) + " ");  
                }  
            }  
        };  
        new Thread(runnableNumber).start();  
        new Thread(runnableChar).start();  
    }  
}

定時器

間隔特定的事件,執行特定的程式。

sleep方法也可以達到同樣的效果,這就是最原始的定時器。

在Java的類庫中已經寫好了一個定時器:java.util.Timer

image-20221029121848231

Timer類的例項方法:

void schedule(TimerTask task, Date firstTime, long period)
//task:指定的任務
//Date firstTime:第一次執行的時間 long delay:間隔多久開始執行
//period:間隔多久執行一次

TimerTask

public abstract class TimerTask extends Object implements Runnable

實現了Runnable介面,是Runnable的抽象實現類。

public class DataBackOptions extends TimerTask {  
    @Override  
    public void run() {  
        LocalDateTime now = LocalDateTime.now();  
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss SSS");  
        String format = now.format(dtf);  
        System.out.println(format + " 執行了一次備份");  
    }  
}

public static void main(String[] args) {  
    Timer t = new Timer(); //一般設定為守護執行緒  
    t.schedule(new DataBackOptions(),1000 * 2,1000 * 2);  
}

/*
2023-11-17 20:22:34 771 執行了一次備份
2023-11-17 20:22:36 779 執行了一次備份
*/

執行緒通訊

多執行緒環境下,我們經常需要多個執行緒的併發和通訊。關於執行緒通訊,最經典的例子就是生產者和消費者的問題。

等待喚醒機制

  • wait():讓當前阻塞等待並釋放鎖,直到另一個執行緒呼叫notify方法或notifyAll方法
  • notify():喚醒因wait阻塞的單個執行緒
  • notifyAll():喚醒因wait阻塞的所有執行緒

其實,所謂喚醒的意思就是讓透過wait()方法進入阻塞的執行緒具備執行資格。必須注意的是,這些方法都是在同步中才有效。同時這些方法在使用時必須標明所屬鎖,這樣才可以明確出這些方法操作的到底是哪個鎖上的執行緒。 仔細檢視API之後,發現這些方法並不定義在Thread中,也沒定義在Runnable介面中,卻被定義在了Object類中,為什麼這些操作執行緒的方法定義在Object類中? 因為這些方法在使用時,必須要標明所屬的鎖,而鎖又可以是任意物件,能被任意物件呼叫的方法一定是定義在Object類中。

  • notifyAll():喚醒在此物件上等待的所有執行緒

其實是對持有該物件鎖的執行緒進行操作

一個執行緒:

  • 滿足條件 -> 處理業務,最終notify
  • 不滿足條件 -> wait

wait()方法有三種形式:無時間引數的wait()方法(一直等待,直到其他執行緒通知),帶毫秒引數的wait()方法和帶毫秒、微秒引數的wait()方法(這兩種方法都是等待指定時間後自動甦醒)。並且呼叫wait()方法的當前執行緒會釋放對該同步監視器的鎖定。 使用wait、notify和notifyAll三個方法必須明白下面幾點:

  1. wait()、notify()和notifyAll()這三個方法都是java.lang.Object類提供的方法。
  2. 使用wait()方法進入等待狀態的執行緒,還會釋放掉鎖,並且只有其它執行緒呼叫notify()或者notifyAll()方法,則進入wait()狀態的鎖才能被喚醒。
  3. wait()、notify()和notifyAll()這三個方法,都必須在同步程式碼塊或同步方法中,並且都必須透過“同步監視器”來呼叫,否則就會丟擲IllegalMonitorStateException異常。

生產者和消費者模式

在該案例中,生產者和消費者是不同種類的執行緒,一個負責存入,另一個負責取出,且它們操作的是同一個資源。但最難的部分在於:

  • 資源到達上限時,生產者等待,消費者消費
  • 資源達到下限時,生產者生產,消費者等待

你會發現,原本互不打擾的兩個執行緒之間開始“溝通”了:

  • 生產者:喂,我這邊做的太多了,先休息會兒,你趕緊消費
  • 消費者:喂,貨快沒了,我休息會兒,你趕緊生產

這種執行緒間的相互排程,也就是執行緒間通訊。

生產者和消費者模式是為了解決特定需求的:

倉庫物件是多執行緒共享的,需要考慮倉庫的執行緒安全問題(生產和消費都涉及到資料更新),倉庫物件最終呼叫wait()notify() 方法,並且是建立在synchronized執行緒同步的基礎之上。

程式碼實現:

這樣做會報錯:java.lang.IllegalMonitorStateException: current thread not owner

《java程式設計思想》第四版一書中有描述到:“執行緒操作的wait()、notify()、notifyAll()方法只能在同步控制方法或同步控制塊內呼叫。如果在非同步控制方法或控制塊裡呼叫,程式能透過編譯,但執行的時候,將得到 IllegalMonitorStateException 異常,並伴隨著一些含糊資訊,比如 ‘當前執行緒不是擁有者’。其實異常的含義是 呼叫wait()、notify()、notifyAll()的任務在呼叫這些方法前必須 擁有(獲取)物件的鎖。”

Java的API文件也有如下描述:

wait()、notify()、notifyAll()方法只應由作為此物件監視器的所有者的執行緒來呼叫。透過以下三種方法之一,執行緒可以成為此物件監視器的所有者:

  1. 透過執行此物件的同步 (Sychronized) 例項方法。
  2. 透過執行在此物件上進行同步的 synchronized 語句的正文。
  3. 對於 Class 型別的物件,可以透過執行該類的同步靜態方法。

所以在呼叫這三個方法時應該同步上下文

單生產者、單消費者、臨界區為1

public class Desk {  
    private List<String> data = new ArrayList<>();  
  
    public Desk() {  
    }  
  
    public Desk(List<String> data) {  
        this.data = data;  
    }  
  
    public synchronized void put(){  
        if (data.isEmpty()){  
            String threadName = Thread.currentThread().getName();  
            String product = threadName + " 生產的一個產品";  
            data.add(product);  
            System.out.println(product);  
            this.notify();  
        }else {  
            try {  
                this.wait();  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
  
    public synchronized void get(){  
        if (data.isEmpty()){  
            try {  
                this.wait();  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }else {  
            String product = data.remove(0);  
            String threadName = Thread.currentThread().getName();  
            System.out.println(threadName + " 消費了:" + product);  
            this.notify();  
        }  
    }
}
public class Test {  
    public static void main(String[] args) {  
        Desk desk = new Desk();  
        Thread producer = new Thread(new Consumer(desk), "Producer");  
        Thread consumer = new Thread(new Producer(desk), "Consumer");  
  
        producer.start();  
        consumer.start();  
    }  
}  
class Consumer implements Runnable{  
    private Desk desk;  
  
    @Override  
    public void run() {  
        for (int i = 0; i < 100; i++) {  
            desk.get();  
  
            try {  
                Thread.sleep(1000);// 避免執行過快  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
    public Consumer() {  
    }  
  
    public Consumer(Desk desk) {  
        this.desk = desk;  
    }  
  
  
  
}  
class Producer implements Runnable{  
    private Desk desk;  
  
    @Override  
    public void run() {  
        for (int i = 0; i < 100; i++) {  
            desk.put();  
  
            try {  
                Thread.sleep(1000);// 避免執行過快  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
  
    public Producer() {  
    }  
  
    public Producer(Desk desk) {  
        this.desk = desk;  
    }  
}

沒有同步可能發生的問題:

對上文程式進行改進:

public synchronized void put(){  
    if (data.isEmpty()){  
        String threadName = Thread.currentThread().getName();  
        String product = threadName + " 生產的一個產品";  
        data.add(product);  
        System.out.println(product);  
        this.notify();  
    }else {  
        try {  
            this.wait();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  
}  
  
public synchronized void get(){  
    if (data.isEmpty()){  
        try {  
            this.wait();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }else {  
        String product = data.remove(0);  
        String threadName = Thread.currentThread().getName();  
        System.out.println(threadName + " 消費了:" + product);  
        this.notify();  
    }  
}

在生產者生產完畢後,喚醒消費者,下一次的迴圈可能生產者搶到執行權並休眠,也可能是消費者搶到執行權並消費,這樣就是浪費了一次執行的機會給到生產者。

public synchronized void put(){  
    if (data.isEmpty()){  
        String threadName = Thread.currentThread().getName();  
        String product = threadName + " 生產的一個產品";  
        data.add(product);  
        System.out.println(product);  
        this.notify();
        this.wait() // <--- 生產完畢自我休眠  
    }else {  
        try {  
            this.wait();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  
}  
  
public synchronized void get(){  
    if (data.isEmpty()){  
        try {  
            this.wait();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }else {  
        String product = data.remove(0);  
        String threadName = Thread.currentThread().getName();  
        System.out.println(threadName + " 消費了:" + product);  
        this.notify();  
        this.wait(); // 消費完畢自我休眠
    }  
}

所以在臨界區為1時,生產完畢可以進行自我休眠

單生產者、單消費者、臨界區 > 1

public synchronized void put(){  
    if (data.size() < 2){  
        String threadName = Thread.currentThread().getName();  
        String product = threadName + " 生產的一個產品";  
        data.add(product);  
        System.out.println(product);  
        this.notify();  
    }else {  
        try {  
            this.wait();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  
}  
  
public synchronized void get(){  
    if (data.isEmpty()){  
        try {  
            this.wait();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }else {  
        String product = data.remove(0);  
        String threadName = Thread.currentThread().getName();  
        System.out.println(threadName + " 消費了:" + product);  
        this.notify();  
    }  
}

此時不能自我休眠,因為臨界區的長度 > 1,生產完畢一個之後還可能繼續生產

多生產者、多消費者、臨界區1

public synchronized void put(){  
    if (data.isEmpty()){  
        String threadName = Thread.currentThread().getName();  
        String product = threadName + " 生產的一個產品";  
        data.add(product);  
        System.out.println(product);  
        this.notifyAll();  //喚醒所有執行緒
        try {  
            this.wait();   //自我休眠
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }else {  
        try {  
            this.wait();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  
}  
  
public synchronized void get(){  
    if (data.isEmpty()){  
        try {  
            this.wait();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }else {  
        String product = data.remove(0);  
        String threadName = Thread.currentThread().getName();  
        System.out.println(threadName + " 消費了:" + product);  
        this.notifyAll();  //喚醒所有執行緒
        try {  
            this.wait();   //自我休眠
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  
}

生產者生產完畢應喚醒所有執行緒,否則可能導致消費者一直休眠

if-else是分支結構,必定有一個執行,所以如果某個執行緒被休眠,只能等待下一次迴圈再進行判斷

  • 另一種形式的多生產、多消費、單臨界
public class Product {  
    private String name;  
    private String color;
}

public class ProductStack {  
    private Product product;  
  
    private boolean flag;  
  
    public ProductStack() {  
    }  
  
    public ProductStack(Product product) {  
        this.product = product;  
    }  
  
    public synchronized void product(Product product){  
        //1. 如果有產品  
        if (flag){  
            try {  
                wait();  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
  
        //2. 如果沒有產品  
  
        this.product = product;  
        notify();  
        this.flag = true;  
        System.out.println("生產者:生產了:" + product);  
    }  
  
    public synchronized void consumer(){  
        if (!flag){  
            try {  
                wait();  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
  
        System.out.println("消費者:消費了:" + product);  
        notify();  
        this.flag = false;  
        product = null;  
    }
}

public class Demo{
	public static void main(String[] args) {  
	    ProductStack productStack = new ProductStack();  
	  
	    new Thread(() -> {  
	        for (int i = 0; i < 100; i++) {  
	            productStack.product(new Product("niger" + i,"black"));  
	        }  
	    }).start();  
	  
	    new Thread(() -> {  
	        for (int i = 0; i < 100; i++) {  
	            productStack.product(new Product("niger" + i,"black"));  
	        }  
	    }).start();  
	  
	    new Thread(() -> {  
	        for (int i = 0; i < 100; i++) {  
	            productStack.consumer();  
	        }  
	    }).start();  
	  
	    new Thread(() -> {  
	        for (int i = 0; i < 100; i++) {  
	            productStack.consumer();  
	        }  
	    }).start();  
	}
}

執行過程:

image.png

本質原因是,消費者在判斷到倉庫中有產品就進入阻塞狀態了,等到被喚醒時進入就緒狀態,而就緒狀態轉到執行狀態沒有再次對倉庫狀態,這裡使用的if結構只在條件成立時執行,所以在休眠被喚醒後,後續的所有的程式碼都會執行。

如果倉庫中已經有產品(另一個生產者生產的),而本執行緒生產時未對倉庫狀態進行判斷,就會覆蓋上一次的產品。

解決辦法:

  1. 改為if-else,if中被休眠,喚醒後也不會執行else,而是在下一輪迴圈再去判斷倉庫狀態
  2. if改為while,被喚醒後再次進行判斷

多生產者、多消費者、臨界區 > 1

public synchronized void put(){  
    if (data.size() < 2){  
        String threadName = Thread.currentThread().getName();  
        String product = threadName + " 生產的一個產品";  
        data.add(product);  
        System.out.println(product);  
        this.notifyAll();  
        //不能自我休眠,可能還需要進行生產  
        //如果要強制交替生產,就需要自我休眠  
    }else {  
        try {  
            this.wait();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  
}  
  
public synchronized void get(){  
    if (data.isEmpty()){  
        try {  
            this.wait();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }else {  
        String product = data.remove(0);  
        String threadName = Thread.currentThread().getName();  
        System.out.println(threadName + " 消費了:" + product);  
        this.notifyAll();  
        //不需要自我休眠  
        //如果交替消費,就要自我休眠  
    }  
}

阻塞佇列實現等待喚醒機制

在佇列中存放生產者生產的產品,如果佇列容量為 1 ,就與上例沒有區別,如果長度>1:

  • 生產者put資料時,如果佇列已滿,進入wait 也稱作 阻塞
  • 消費者take資料時,如果佇列空, 進入wait,也稱作 阻塞

阻塞佇列的繼承結構

classDiagram class Iterable{ <<interface>> } class Collection{ <<interface>> } Iterable <|-- Collection class Queue{ <<interface>> } Collection <|-- Queue class BlockingQueue{ <<interface>> } Queue <|-- BlockingQueue class ArrayBlockingQueue{ } class LinkedBlockingQueue{ } BlockingQueue <|.. ArrayBlockingQueue BlockingQueue <|.. LinkedBlockingQueue note for ArrayBlockingQueue "底層是陣列,有界" note for LinkedBlockingQueue "底層是連結串列,無界(不超過int最大值)"
  • 生產者和消費者必須使用同一個阻塞佇列
class Producer implements Runnable{  
    private ArrayBlockingQueue<String> abq;  
  
    public Producer(ArrayBlockingQueue<String> abq) {  
        this.abq = abq;  
    }  
  
    @Override  
    public void run() {  
        for (int i = 0; i < 100; i++) {  
            try {  
                abq.put("product");  
                System.out.println("生產完畢");  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
}  
class Consumer implements Runnable{  
    private ArrayBlockingQueue<String> abq;  
  
    public Consumer(ArrayBlockingQueue<String> abq) {  
        this.abq = abq;  
    }  
  
    @Override  
    public void run() {  
        for (int i = 0; i < 100; i++) {  
            try {  
                abq.take();  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
}

阻塞佇列將同步操作封裝在put和take方法中了,具體可見[[012-多執行緒的思考#輪詢|模擬阻塞佇列]]

主執行緒:

ArrayBlockingQueue<String> abq = new ArrayBlockingQueue<>(1); //必須指定初始化容量  
Thread producer = new Thread(new Producer(abq));  
Thread consumer = new Thread(new Consumer(abq));  
producer.start();  
consumer.start();

在控制檯上的列印可能有亂序的情況,因為阻塞佇列將同步的操作封裝在put和take方法中,put一結束可能被其他執行緒搶走執行權

put方法:

public void put(E e) throws InterruptedException {  
    Objects.requireNonNull(e);  
    final ReentrantLock lock = this.lock;  
    lock.lockInterruptibly();  
    try {  
        while (count == items.length)  
            notFull.await();  
        enqueue(e);  
    } finally {  
        lock.unlock();  
    }  
}

將加鎖和釋放鎖的操作放在put和take時進行了,如果在put或take後再進行列印,列印語句可能是亂序的,但是資料一定是正常生產和消費的

練習

  • 兩個執行緒分別列印奇數和偶數
  1. 兩個執行緒對一個Num操作
  2. 一個生產者,兩個消費者?

  • 共有1000張電影票可以在兩個視窗領取,假設每次領取的時間為3000毫秒

要求:請用多執行緒模擬賣票過程並列印剩餘電影票的數量


  • 有100份禮品,兩人同時傳送,當剩下的禮品小於10份的時候則不再送出。利用多執行緒模擬該過程並將執行緒的名字和禮物的剩餘數量列印出來

  • 同時開啟兩個執行緒,共同獲取1-100之間的所有數字。要求:將輸出所有的奇數。

  • 假設:100塊,分成了3個包,現在有5個人去搶。

其中,紅包是共享資料,5個人是5條執行緒。

列印結果如下
XXX搶到了XXX元
XXX搶到了XXX元
XXX搶到了XXX元
XXX沒搶到
XXX沒搶到

抽獎

  • 有一個抽獎池,該抽獎池中存放了獎勵的金額,該抽獎池中的獎項為

建立兩個抽獎箱(執行緒)設定執行緒名稱分別為“抽獎箱1”,“抽獎箱2"隨機從抽獎池中獲取獎項元素並列印在控制檯上,格式如下

# 每次抽出一個獎項就列印一個(隨機)
抽獎箱1 又產生了一個 10 元大獎
抽獎箱1 又產生了一個 100 元大獎
抽獎箱1 又產生了一個 200 元大獎
抽獎箱1 又產生了一個 800 元大獎
抽獎箱2 又產生了一個 700 元大獎

  • 在上題基礎上完成如下需求

每次抽的過程中,不列印,抽完時一次性列印(隨機)

在此次抽獎過程中,抽獎箱1總共產生了6個獎項
	分別為:10,20,100,500,2,300最高獎項為300元,總計額為932元
在此次抽獎過程中,抽獎箱2總共產生了6個獎項
	分別為:5,50,200,800,80,700最高獎項為800元,總計額為1835元

  • 在上題基礎上完成如下需求
在此次抽獎過程中,抽獎箱1總共產生了6個獎項
	分別為: 10,20,100,500,2,300最高獎項為300元,總計額為932元
在此次抽獎過程中,抽獎箱2總共產生了6個獎項
	分別為:5.50,200,800,80,700,最高獎項為800元,總計額為1835元
在此次抽獎過程中,抽獎箱2中產生了最大獎項,該獎項金額為800元

猜數字

用兩個執行緒玩猜數字遊戲,第一個執行緒負責隨機給出1~100之間的一個整數,第二個執行緒負責猜出這個數。 要求:

1. 每當第二個執行緒給出自己的猜測後,第一個執行緒都會提示“猜小了”、“猜 大了”或“猜對了”。
    
2. 猜數之前,要求第二個執行緒要等待第一個執行緒設定好 要猜測的數。
    
3. 第一個執行緒設定好猜測數之後,兩個執行緒還要相互等待,其原則是:

    第二個執行緒給出自己的猜測後,等待第一個執行緒給出的提示;
    第一個 執行緒給出提示後,等待第二個執行緒給出猜測,如此進行,直到第二個執行緒給 出正確的猜測後,兩個執行緒進入死亡狀態。
class Guess {  
    private Integer number;  
    private String guess;  
  
    public Guess() {  
    }  
      
    public Integer getNumber() {  
        return number;  
    }  
  
    public void setNumber(Integer number) {  
        this.number = number;  
    }  
  
    public String getGuess() {  
        return guess;  
    }  
   
    public void setGuess(String guess) {  
        this.guess = guess;  
    }   
}
class GenerateNumberThread implements Runnable {  
    private Guess guess;  
  
    public GenerateNumberThread() {  
    }  
  
    public GenerateNumberThread(Guess guess) {  
        this.guess = guess;  
    }  
  
    @Override  
    public void run() {  
        synchronized (guess) {  
            int number = 0;  
            Random random = new Random();  
            if (guess.getNumber() == null) {  //如果還沒開始猜,先
                number = random.nextInt(0, 100) + 1;  
                System.out.println(Thread.currentThread().getName() + " 生成的數字是:" + number);  
                try {  
                    guess.notify();  
                    guess.wait(); // 第一次生成結果後休眠  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
  
            while (true) {  
                if (guess.getNumber() > number){  
                    guess.setGuess("big");  
                    System.out.println(Thread.currentThread().getName() + " 猜大了");  
                    guess.notify();  
                    try {  
                        guess.wait();  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                } else if (guess.getNumber() < number) {  
                    guess.setGuess("small");  
                    System.out.println(Thread.currentThread().getName() + " 猜小了");  
                    guess.notify();  
                    try {  
                        guess.wait();  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                }else {  
                    guess.setGuess("equals");  
                    guess.notify();  
                    break;  
                }  
            }  
  
            System.out.println(Thread.currentThread().getName() + ":數字 " + number + "被猜到了");  
        }  
    }  
  
}
class GuessNumberThread implements Runnable {  
    private Guess guess;  
  
    public GuessNumberThread() {  
    }  
  
    public GuessNumberThread(Guess guess) {  
        this.guess = guess;  
    }  
  
  
    @Override  
    public void run() {  
        synchronized (guess){  
            Random random = new Random();  
  
            if (guess.getNumber() == null){  // 如果第一次,生成第一次猜的結果
  
                guess.setNumber(random.nextInt(0,100) + 1);  
  
                guess.notify();  
                try {  
                    guess.wait();  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
  
            while (true){  
  
                if (guess.getGuess() == null){  
                    guess.notify();  
                    try {  
                        guess.wait();  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                }  
  
                if (guess.getGuess().equals("big")){  
                    int number = random.nextInt(1, guess.getNumber());  
                    System.out.println(Thread.currentThread().getName() + " 猜的數字是:" + number);  
                    guess.setNumber(number);  
                    guess.notify();  
                    try {  
                        guess.wait();  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                } else if (guess.getGuess().equals("small")) {  
                    int number = random.nextInt(guess.getNumber() + 1, 101);  
                    System.out.println(Thread.currentThread().getName() + " 猜的數字是:" + number);  
                    guess.setNumber(number);  
                    guess.notify();  
                    try {  
                        guess.wait();  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                }else if (guess.getGuess().equals("equals")){  
                    System.out.println(Thread.currentThread().getName() + 
								                    " 猜到了數字:" + guess.getNumber());  
                    break;  
                }  
            }  
        }  
    }  
  
}
public static void main(String[] args) {  
    Guess guess = new Guess();  
    new Thread(new GenerateNumberThread(guess),"GenerateNumberThread").start();  
    new Thread(new GuessNumberThread(guess),"GuessNumberThread").start();  
}

Lock

之前學習瞭如何使用synchronized關鍵字來進行同步訪問,在JDK1.5之後,java.util.concurrent.locks包下提出了另一種方式來實現同步訪問,那就是Lock

又提供了Lock類是因為synchronized是有缺陷的。在多生產者多消費者問題中,我們透過while判斷和notifyAll()全喚醒方案來解決問題,但是notifyAll()同時也帶來了弊端,它要喚醒所有的被等待的執行緒,意味著既喚醒了對方,也喚醒了本方。
在喚醒本方執行緒後還要不斷判斷標記,就降低了程式的效率。

如果我們希望只喚醒對方的執行緒,而不喚醒本方執行緒,那麼我們可以使用JDK1.5以後提供的Lock鎖。

另外,雖然我們可以理解同步程式碼塊和同步方法的鎖物件問題,但是我們並沒有直接看到在哪裡加上了鎖,在哪裡釋放了鎖,因為synchronized對於鎖的操作是隱式的。 同步程式碼塊的加鎖和釋放鎖位置:

synchronized (同步監視器) { // 加鎖位置
	// 需要同步的程式碼
} // 釋放鎖位置

同步方法的加鎖和釋放鎖位置:

[修飾符] synchronized 返回值型別 方法名(形參列表) { // 加鎖位置
	// 需要被同步的程式碼
} // 釋放鎖位置

為了更清晰的表達如何加鎖和釋放鎖,我們可以使用JDK1.5以後提供的Lock鎖。Lock將同步和鎖封裝成了物件,並將加鎖和釋放鎖變為了顯示動作,這個是synchronized無法辦到的。總之,Lock鎖實現提供了比使用synchronized方法和程式碼塊更廣泛的鎖定操作。

總結一下,也就是說Lock提供了比synchronized更多的功能。但是要注意以下幾點:

  1. Lock不是Java語言內建的,synchronized是Java語言的關鍵字,因此是內建特性;Lock是一個介面,透過Lock介面的實現類可以實現同步訪問。
  2. synchronized不需要使用者去手動釋放鎖,當synchronized方法或者synchronized程式碼塊執行完之後,系統會自動讓執行緒釋放對鎖的佔用;而Lock則必須要使用者去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現死鎖現象。

Lock介面詳解

Lock就是一個介面,位於java.util.concurrent.locks包中。

Lock介面中有兩個重要的方法,分別是lock()方法和unlock()方法,lock()方法用來獲取鎖,unLock()方法是用來釋放鎖。

  • lock():用來獲取鎖。如果鎖已被其它執行緒獲取,則進行等待。

如果採用Lock,必須主動去釋放鎖,並且在發生異常時,不會自動釋放鎖。因此一般來說,使用Lock必須在try{}catch{}塊中進行,並且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發生。 通常使用Lock來進行同步的話,是以下面這種形式去使用的:

Lock lock = ...;
lock.lock(); // 獲取鎖
try{
	// 需要處理任務程式碼
} catch(Exception e) {
	// 處理異常的操作 
} finally {
	lock.unlock(); // 釋放鎖
}

ReentrantLock類詳解

ReentrantLock,意思是“可重入鎖”。ReentrantLock類是Lock介面的實現類,並且ReentrantLock類中提供了更多的實用方法。

如果某個班次的列車一共有100張火車票需要賣,火車站為了提高買票的效率,安排了三個售票視窗來進行賣票。從程式設計的角度上來講,三個賣票視窗就是開啟了三個執行緒,三個執行緒併發訪問同一個共享資料(也就是100張票)。

// 賣票執行緒任務類
class TicketRunnable implements Runnable {
	// 共享資料,儲存火車票的總量
	private static int ticketNum = 100; 	
	@Override
	public void run() {
		while (true) {
			// 當票數小於等於0時,視窗停止售票,跳出死迴圈
			if (ticketNum <= 0)
				break;
			// 當票數大於0時,售票視窗開始售票
			try {
				Thread.sleep(10); // 模擬切換執行緒的操作
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			// 輸出售票視窗(執行緒名)賣出的哪一張票
			String name = Thread.currentThread().getName();
			System.out.println(name + "--賣出第" + ticketNum + "張票");
			// 賣票之後,總票數遞減
			ticketNum--;
		}
	}
}
// 測試類
public class Test {
	public static void main(String[] args) {
		// 建立執行緒任務類物件
		TicketRunnable tr = new TicketRunnable();
		// 開啟三個售票執行緒
		new Thread(tr, "視窗1").start();
		new Thread(tr, "視窗2").start();
		new Thread(tr, "視窗3").start();
	}
}

這樣就會導致執行緒不安全,因為對共享資料的修改並未進行同步。

三個執行緒併發訪問同一個共享資料(也就是100張票),產生的後果就是“髒讀”。

為了解決這個問題,我們需要實現對共享資料(也就是100張票)的同步訪問,這樣就避免了多執行緒引發的執行緒安全問題。 實現對部分程式碼的同步,我們可以使用同步程式碼塊來實現,也可以透過ReentrantLock鎖來解決。接下來我們就使用ReentrantLock鎖來解決賣票的問題,以上案例中只需要實現TicketRunnable類中賣票程式碼的新增同步即可。

class TicketRunnable implements Runnable {  
    private static int ticketNum = 100;  
    private static Lock lock = new ReentrantLock();  //鎖物件必須是要同步的執行緒共享的
													 //因為建立了三個TicketRunnable,所以就要static修飾
  
    @Override  
    public void run() {  
        while (true) {  
            try {  
                lock.lock();  
                if (ticketNum <= 0) {  
                    break;  
                }  
                Thread.sleep(10);  
                String name = Thread.currentThread().getName();  
                System.out.println(name + "賣了第" + (--ticketNum) + "張票");  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            } finally {  
                lock.unlock();  
            }  
  
  
        }  
    }  
}

多生產、多消費者問題

多生產者多消費者帶來的問題,我們可以透過while判斷和notifyAll()全喚醒方案來解決,但是notifyAll()全喚醒也帶來了弊端,它要喚醒所有的被等待的執行緒,意味著既喚醒了對方,也喚醒了本方,在喚醒本方執行緒後還要不斷判斷標記,就降低了程式的效率。 這些問題在JDK1.5之後給出瞭解決方案,如果我們希望只喚醒對方的執行緒,而不喚醒本方執行緒,那麼我們可以使用Lock鎖。 接下來,我們就基於【生產者消費者案例】中的倉庫類(ProductStack類)的程式碼進行修改,把synchronized修飾的方法改為Lock鎖來實現同步。

原先的程式碼:

// 倉庫類
public class ProductStack {

	private Product product;

	private boolean flag; 

	public ProductStack(Product product) {
		this.product = product;
	}

	public synchronized void product(String name, String color) {

		while(flag) { 
			try {
				this.wait(); // 該生產者執行緒進入執行緒池等待
			} catch (InterruptedException e) {
				e.printStackTrace();
			} 
		}

		product.setName(name);
		try {
			// 執行緒等待,主要作用是為了切換執行緒
			Thread.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		product.setColor(color);
		System.out.println("生產者----->" + color + name);

		this.flag = true;

		this.notifyAll(); //問題:可能下一次生產者又搶到執行權
	}
	// 消費商品方法
	public synchronized void consume() {

		while(!flag) { 
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			} 
		}

		System.out.println("消費者-->" + product.getColor() + product.getName());

		this.flag = false;

		this.notifyAll(); 
	}
}

改進:

// 倉庫類  
class ProductStack {  
  
    private Product product;  
  
    private boolean flag; // 預設值為false  
    // 建立一個鎖物件  
    Lock lock = new ReentrantLock();  
  
    public ProductStack(Product product) {  
        this.product = product;  
    }  
  
    public /*synchronized*/ void product(String name, String color) {  
        lock.lock(); // 獲取鎖  
        try {  
            while (flag)  
                this.wait();  
  
            product.setName(name);  
            product.setColor(color);  
            System.out.println("生產者----->" + color + name);  
            this.flag = true;  
            this.notifyAll();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        } finally {  
            lock.unlock(); // 釋放鎖  
        }  
    }  
    // 消費商品方法  
    public /*synchronized*/ void consume() {  
        lock.lock(); // 獲取鎖  
        try {  
            while (!flag)  
                this.wait();  
  
            System.out.println("消費者-->" + product.getColor() + product.getName());  
            this.flag = false;  
            notifyAll();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        } finally {  
            lock.unlock(); // 釋放鎖  
        }  
    }  
}

但是執行該方法就出現了異常:Exception in thread "Thread-0" Exception in thread "Thread-1" java.lang.IllegalMonitorStateException: current thread is not owner

當前執行緒的鎖物件和呼叫wait()方法和notifyAll()方法的物件不一致的時候造成,這時就會丟擲java.lang.IllegalMonitorStateException異常。

以上案例中,我們用到的鎖是Lock,而不是同步方法中的this,在程式碼中呼叫wait()、notifyAll()方法的鎖依舊是this,這就是丟擲異常的根源。

在JDK1.5的新特性中,使用Lock鎖替代了同步方法和同步程式碼塊,使用Condition的方法替代了Object提供的等待喚醒的方法。在Condition介面中,用await()替換wait()用signal()替換notify()用signalAll()替換notifyAll(),這些方法與Lock鎖配合使用也可以實現等待/喚醒機制。

Condition可以替代傳統的執行緒間通訊,Condition定義了等待/通知兩種型別的方法,當前執行緒呼叫這些方法時,需要提前獲取到Condition物件關聯的鎖

Condition物件是由Lock物件(呼叫Lock物件的newCondition()方法)獲取出來的,換句話說,Condition是依賴Lock物件的。另外,Condition介面可以支援多個等待佇列,也就是說透過一個Lock物件,我們可以獲取多個Condition物件。

class ProductStack {  
  
    private Product product;  
  
    private boolean flag; // 預設值為false  
    // 建立一個鎖物件  
    Lock lock = new ReentrantLock();  
  
    //生產者等待佇列  
    Condition conditionPro = lock.newCondition();  
  
    //消費者等待佇列  
    Condition conditionCon = lock.newCondition();  
  
    public ProductStack(Product product) {  
        this.product = product;  
    }  
  
    public void product(String name, String color) {  
        lock.lock(); // 獲取鎖  
        try {  
            while (flag)  
                //this.wait();  
                conditionPro.await(); //進入生產者佇列等待  
  
            product.setName(name);  
            product.setColor(color);  
            System.out.println("生產者----->" + color + name);  
            this.flag = true;  
            conditionCon.signalAll(); //喚醒所有消費者  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        } finally {  
            lock.unlock(); // 釋放鎖  
        }  
    }  
    // 消費商品方法  
    public void consume() {  
        lock.lock(); // 獲取鎖  
        try {  
            while (!flag)  
                conditionCon.await(); //進入消費者佇列等待  
            System.out.println("消費者-->" + product.getColor() + product.getName());  
            this.flag = false;  
            conditionPro.signalAll(); //喚醒所有生產者  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        } finally {  
            lock.unlock(); // 釋放鎖  
        }  
    }  
}

執行緒池

在前面的章節中,我們使用執行緒的時候就去建立一個執行緒,這樣實現起來非常簡便,但是就會有一個問題: 如果併發的執行緒數量很多,並且每個執行緒都是執行一個時間很短的任務就結束了,這樣頻繁建立執行緒就會大大降低系統的效率,因為系統啟動一個新執行緒的成本是比較高的,它涉及到與作業系統的互動,頻繁建立執行緒和銷燬執行緒都需要時間。

在這種情況下,使用執行緒池可以很好的提供效能,尤其是當程式中需要建立大量生存期很短暫的執行緒時,更應該考慮使用執行緒池。

執行緒池在提交任務時建立核心執行緒,程式將一個Runnable物件傳給執行緒池,執行緒池就會啟動一條執行緒來執行該物件的run()方法,當run()方法執行結束後,該執行緒並不會死亡,而是再次返回執行緒池中成為空閒狀態,等待執行下一個Runnable物件的run()方法。

注意,執行緒池是在提交任務時開始建立核心執行緒,即便核心執行緒1已經工作結束,再次提交任務也會建立核心執行緒2來執行,直到核心執行緒數量達到最大值

除此之外,使用執行緒池可以有效地控制系統中併發執行緒的數量,但系統中包含大量併發執行緒時,會導致系統效能劇烈下降,甚至導致JVM崩潰。而執行緒池的最大執行緒數引數可以控制系統中併發的執行緒不超過此數目。

合理利用執行緒池能夠帶來三個好處:

  1. 降低資源消耗。透過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗,避免系統資源耗盡。
  2. 提高響應速度。當任務到達時,任務可以不需要等到執行緒建立就能立即執行。
  3. 提高執行緒的可管理性。執行緒是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一的分配,調優和監控。

在JDK1.5之前,必須手動的實現自己的執行緒池,從JDK1.5之後,Java內建支援執行緒池。與多執行緒併發的所有支援的類都在java.lang.concurrent包中,我們可以使用裡面的類更加的控制多執行緒的執行。

建立執行緒池

  • JDK5提供的代表執行緒池的介面 ExecutorService

如何得到執行緒池物件?

  • 方式一:使用ExecutorService的實現類ThreadPoolExecutor建立一個執行緒池物件
public ThreadPoolExecutor(int corePoolSize,                    //核心執行緒數量
                          int maximumPoolSize,                 //指定執行緒池最大執行緒數量
                          long keepAliveTime,                  //指定臨時執行緒存活時間
                          TimeUnit unit,                       //指定臨時執行緒存活的時間單位
                          BlockingQueue<Runnable> workQueue,   //指定執行緒池的任務佇列
                          ThreadFactory threadFactory,         //指定執行緒池的執行緒工程 
                          RejectedExecutionHandler handler) {  //指定執行緒池任務拒絕策略
new ThreadPoolExecutor(3, 5, 1, TimeUnit.MINUTES,  
        new LinkedBlockingQueue<>(),  
        new ThreadFactory() {  //建立ThreadFactory的實現類
            @Override  
            public Thread newThread(Runnable r) {  
                return new Thread(r);  
            }  
        },  
        new ThreadPoolExecutor.AbortPolicy()  //拒絕策略
        );
  • 我們建立的實現類就是直接new Thread返回,可以使用Executors.defaultThreadFactory()替代:
new ThreadPoolExecutor(3, 5, 1, TimeUnit.MINUTES,  
        new LinkedBlockingQueue<>(),  
        Executors.defaultThreadFactory(),  
        new ThreadPoolExecutor.AbortPolicy()  
        );

底層:

public static ThreadFactory defaultThreadFactory() {  
    return new DefaultThreadFactory();  //也是new Thread返回
}
  • 任務拒絕策略:

image-20230413153533965

每個任務拒絕策略其實都是一個靜態內部類:

public class ThreadPoolExecutor extends AbstractExecutorService {

	public static class AbortPolicy implements RejectedExecutionHandler {  
    public AbortPolicy() { }  

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {  
        throw new RejectedExecutionException("Task " + r.toString() +  
                                             " rejected from " +  
                                             e.toString());  
	    }  
	}

}

建立的時候就是new 外部類.內部類()

  • 任務等待阻塞佇列有兩種:
  1. ArrayBlockingQueue:必須指定等待佇列長度
  2. LinkedBlockingQueue:無限長的等待佇列

執行緒池常用方法

  • void execute(Runnable command) :執行Runnable任務

  • Future<T> submit(Callable<T> task) :執行Callable任務

  • void shutdown() :全部任務執行完畢再關閉執行緒池

  • List<Runnable> shotdownNow() : 立刻關閉執行緒池,停止正在執行的任務,返回任務佇列中未執行的任務

執行緒池處理Runnable任務

測試:

執行緒池中即便有空閒核心執行緒,每次提交任務也會建立新的核心執行緒,直到核心執行緒滿

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(3, 5, 1, TimeUnit.MINUTES,  
        new LinkedBlockingQueue<>(),  
        Executors.defaultThreadFactory(),  
        new ThreadPoolExecutor.AbortPolicy()  
);  
  
Runnable runnable = () -> {  
    for (int i = 0; i <= 3; i++) {  
        System.out.println(Thread.currentThread().getName() + " ===> " + i);  
    }  
};  
  
threadPool.execute(runnable); // < --- pool size = 1  
threadPool.execute(runnable); // < --- pool size = 2  
threadPool.execute(runnable); // < --- pool size = 3

注意:

  • 臨時執行緒的開啟:核心執行緒都在忙,任務佇列滿,就會開啟臨時執行緒
  • 拒絕策略觸發:核心和臨時執行緒都在忙,任務佇列滿,就會觸發任務拒絕策略

測試:

  1. 保持核心執行緒休眠,建立臨時執行緒
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(3, 5, 1, TimeUnit.MINUTES,  
        new ArrayBlockingQueue<>(3), //只有ArrayBlockingQueue才會達到上限  
        Executors.defaultThreadFactory(),  
        new ThreadPoolExecutor.AbortPolicy()  
);  
  
Runnable runnable = () -> {  
    try {  
            System.out.println(Thread.currentThread().getName());  
            Thread.sleep(1000 * 365);  
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    }  
};  
  
threadPool.execute(runnable); // < --- print: pool-1-thread-1  ->  pool size = 1  
threadPool.execute(runnable); // < --- print: pool-1-thread-2  ->  pool size = 2  
threadPool.execute(runnable); // < --- print: pool-1-thread-3  ->  pool size = 3  
threadPool.execute(runnable); // < --- none-print  blockQueue size = 1  
threadPool.execute(runnable); // < --- none-print  blockQueue size = 2  
threadPool.execute(runnable); // < --- none-print  blockQueue size = 3

核心執行緒3,等待佇列3,不會建立臨時執行緒

  1. 核心執行緒休眠,任務佇列滿,建立臨時執行緒
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(3, 5, 1, TimeUnit.MINUTES,  
        new ArrayBlockingQueue<>(3), //只有ArrayBlockingQueue才會達到上限  
        Executors.defaultThreadFactory(),  
        new ThreadPoolExecutor.AbortPolicy()  
);  
  
Runnable runnable = () -> {  
    try {  
            System.out.println(Thread.currentThread().getName());  
            Thread.sleep(1000 * 365);  
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    }  
};  
  
threadPool.execute(runnable); // < --- print: pool-1-thread-1  ->  pool size = 1  
threadPool.execute(runnable); // < --- print: pool-1-thread-2  ->  pool size = 2  
threadPool.execute(runnable); // < --- print: pool-1-thread-3  ->  pool size = 3  
threadPool.execute(runnable); // < --- none-print into blockQueue size = 1  
threadPool.execute(runnable); // < --- none-print into blockQueue size = 2  
threadPool.execute(runnable); // < --- none-print into blockQueue size = 3  
threadPool.execute(runnable); // < --- print: pool-1-thread-4 aka temp-thread-1  
threadPool.execute(runnable); // < --- print: pool-1-thread-5 aka temp-thread-2

任務佇列達到上限,核心執行緒忙,建立臨時執行緒處理新加入的任務

驗證處理新加入的任務:為每個任務設定編號並輸出:

class MyRunnable implements Runnable{  
    private String name;  
  
    public MyRunnable(String name) {  
        this.name = name;  
    }  
  
    @Override  
    public void run() {  
        try {  
            System.out.println(Thread.currentThread().getName() + " execute " + name);  
            Thread.sleep(1000 * 365);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  
}
threadPool.execute(new MyRunnable("runnable1")); //pool-1-thread-1 execute runnable1 -> core thread1  
threadPool.execute(new MyRunnable("runnable2")); //pool-1-thread-2 execute runnable2 -> core thread2  
threadPool.execute(new MyRunnable("runnable3")); //pool-1-thread-3 execute runnable3 -> core thread3  
threadPool.execute(new MyRunnable("runnable4")); //none-print,into-blockQueue  
threadPool.execute(new MyRunnable("runnable5")); //none-print,into-blockQueue  
threadPool.execute(new MyRunnable("runnable6")); //none-print,into-blockQueue  
threadPool.execute(new MyRunnable("runnable7")); //pool-1-thread-4 execute runnable7 -> temp-thread1  
threadPool.execute(new MyRunnable("runnable8")); //pool-1-thread-5 execute runnable8 -> temp-thread2
  1. 觸發任務拒絕策略:
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(3, 5, 1, TimeUnit.MINUTES,  
        new ArrayBlockingQueue<>(3), //只有ArrayBlockingQueue才會達到上限  
        Executors.defaultThreadFactory(),  
        new ThreadPoolExecutor.AbortPolicy()  
);  
  
  
  
threadPool.execute(new MyRunnable("runnable1")); //pool-1-thread-1 execute runnable1 -> core thread1  
threadPool.execute(new MyRunnable("runnable2")); //pool-1-thread-2 execute runnable2 -> core thread2  
threadPool.execute(new MyRunnable("runnable3")); //pool-1-thread-3 execute runnable3 -> core thread3  
threadPool.execute(new MyRunnable("runnable4")); //none-print,into-blockQueue  
threadPool.execute(new MyRunnable("runnable5")); //none-print,into-blockQueue  
threadPool.execute(new MyRunnable("runnable6")); //none-print,into-blockQueue  
threadPool.execute(new MyRunnable("runnable7")); //pool-1-thread-4 execute runnable7 -> temp-thread1  
threadPool.execute(new MyRunnable("runnable8")); //pool-1-thread-5 execute runnable8 -> temp-thread2  
threadPool.execute(new MyRunnable("runnable9")); //觸發任務拒絕策略  
/**  
 * Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task thread_pool_test.MyRunnable@66a29884 rejected from java.util.concurrent.ThreadPoolExecutor@3d494fbf * [Running, pool size = 5, active threads = 5, queued tasks = 3, completed tasks = 0] */

執行緒池處理Callable任務

ThreadLocal

同一個執行緒中共享的變數。

相關文章