Java併發實戰一:執行緒與執行緒安全

YC發表於2021-07-13

從零開始建立一家公司

Java併發程式設計是Java的基礎之一,為了能在實踐中學習併發程式設計,我們跟著建立一家公司的旅途,一起來學習Java併發程式設計。

程式與執行緒

由於我們的目標是學習併發程式設計,所以我不會把很多時間放在底層原理和複雜的概念上。作業系統上的程式就像是全國各地的公司,而每個公司又都有許多員工--執行緒。關於程式與執行緒的關係先了解這麼多。

建立一個執行緒

想象你現在成立了一個網際網路公司,你準備先設立一個總經理的崗位,而你自己是幕後Boss,Main執行緒。

public class App {
    public static void main(String[] args) throws Exception {
        Thread manager = new Thread(new manager());
        manager.start();        
    }
}
class manager implements Runnable {    
    @Override
    public void run() {
        System.out.println("我是總經理");
    }
}

讓我們一步步看上面的程式碼,首先為了區分公司的員工和其他人,每個員工要有統一的標識,都屬於 Thread 類。這個Thread就是我們公司的員工標識,只要是這個類的物件都屬於你的公司。

雖然已經有了 Thread 標識,但公司的每個人的職責都不同,所以還要進一步的細分。向Thread建構函式傳入不同的實現Runnable介面的物件,可以獲得不同職責的員工。

不同員工的職責由不同的 run 方法實現區分。manager.start()方法就是預設呼叫Runnable介面中的run方法。同時Runnable之所以是個介面,是因為繼承只能單一,而介面可以實現多個,就像我們公司的員工,在社會上還可能有其他位置一樣。

終止執行緒

我們公司當然要有下班制度,重新實現如下。

public class App {
    public static void main(String[] args) throws Exception {
        Thread manager = new Thread(new manager());
        manager.start();   
        manager.interrupt();
    }
}
class manager implements Runnable {    
    @Override
    public void run() {
        while(true) {
            if(Thread.currentThread().isInterrupted()) break;
            System.out.println("正在上班中");
        }
} }

可以看到 manager 類中加了個迴圈表示持續的上班狀態。當到下班時間時,你(Main執行緒)呼叫經理的 interrupt 方法通知他該下班了。

注意,這裡的 interrupt方法不會直接結束上班狀態,只是通知。而經理根據自己 run 方法的實現來決定到底怎麼下班。

用  Thread.currentThread().isInterrupted() 方法來判斷是否收到通知。 Thread.currentThread()代表當前物件,之所以不是當前類,是因為你有時候會想只通知某些特定的員工下班,而不是每次通知都只能讓所有員工下班。

還有一個 interrupted 方法,和 isInterrupted 作用相同,都是查詢當前狀態,不過前一個方法查詢的同時會清除狀態。如果員工都是收到中斷請求就下班,那二者沒有什麼區別。但對某些需要收到特定次數下班通知才會下班的員工來說,用 interrupted方法就特別合適。

執行緒休眠

當員工在上班時間卻感覺疲倦怎麼辦,幸好我們有休息制度。

class manager implements Runnable {    
    @Override
    public void run() {
        while(true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                break;
            }
            if(Thread.currentThread().isInterrupted()) break;
            System.out.println("我是總經理");
        }
    }
}

可以看到程式碼中增加了靜態方法 Thread.sleep 和一個異常處理機制。執行緒會先休眠1秒後在執行下面的步驟。

雖然 sleep 是靜態方法,但是隻對當前執行緒其作用。這樣設計的原因是為了防止別的執行緒呼叫該執行緒的休眠方法,也就是說,只有當前執行緒才能控制當前執行緒的休眠狀態。

由於執行緒休眠過程中無法處理中斷,所以當執行緒休眠時收到中斷請求,就會丟擲異常,在異常處理中決定如何中斷。

總結

關於執行緒的基本情況基本這麼多,可以看到麻雀雖小,五臟俱全。有唯一的標識Thread,可以實現自己的方法,可以響應中斷,可以進行休眠等待。一個員工的工作週期已經初步成型,接下來我們看看簡單的員工之間的合作。

等待(wait)和通知(notify)

等待與通知這兩個方法和前面介紹的最大的不同在於,由於要負責執行緒之間的協作,這兩個方法是屬於object的而不是Thread的。

什麼意思呢?這使得可以呼叫  Main 執行緒中的物件的wait方法來中斷 Main執行緒。

具體過程如下 當一個執行緒呼叫等待方法時,它會加入一個等待佇列。由於有多個執行緒可能擁有該物件,當不同執行緒先後呼叫這個方法時,都會加入這個物件的等待佇列中。

當 notify 方法被某一執行緒呼叫時,就會在這個等待佇列中隨機挑出一個執行緒喚醒(並不是先到先得)。

要注意的是 使用 wai() 和 notify 的關鍵字必須要加鎖,程式碼加粗部分

public class App {
    public static void main(String[] args) throws Exception {
        Thread manager = new Thread(new manager());
        manager.start();   
        manager.interrupt();
        synchronized(manager){ manager.notify(); }
    }
}
class manager implements Runnable {    
    @Override
    public void run() {
        while(true) {
            synchronized(this) {
            try {
this.wait(); Thread.sleep(
1000); } catch (InterruptedException e) { break; } if(Thread.currentThread().isInterrupted()) break; System.out.println("我是總經理"); } } } }

 如果不加鎖編譯不會報錯,但執行會會有 current thread is not owner 異常,意思是當前執行緒沒有獲得物件的鎖就呼叫了 wait 方法,notify 的方法同理,也必須要先獲取鎖才能執行。

過程就是 加鎖---- 等待(wait)-----釋放鎖 -----加鎖------通知(notify)------ 繼續執行。但通知後不會釋放鎖,所以呼叫 wait 方法的執行緒要先等該執行緒執行完釋放鎖後才能繼續執行。

至於為什麼要加鎖呢?如果沒有加鎖,就會出現丟失喚醒問題,既 notify 方法早於 wait 呼叫,導致 wait 的執行緒一直接收不到喚醒訊號。

為什麼加鎖就能避免,難道 notify 執行緒沒有可能先執行嗎?其實確實即使加鎖後 notify 也可能先於 wait 執行。因為這兩個方法是負責執行緒協作的,所以一般程式碼邏輯是使用者來寫出,使用者來避免 notify 先於wait執行,但如果沒有加鎖,即使使用者的邏輯正確也可能導致 notify 先於 wait 執行,這也是個併發問題。

等待執行緒結束(join)和謙讓(yeild)

等待結束和謙讓是另外的一種執行緒間協作的方式,上文提到的等待和通知是基於執行緒內部方法的,而等待結束是等待執行緒整體的,可以說是執行緒協作的一種補充。

看個程式碼

public class App {
    public static void main(String[] args) throws Exception {
        Thread manager = new Thread(new manager());
        manager.start();
        manager.join();
        }
    }
}
class manager implements Runnable {    
    @Override
    public void run() {
           Thread.sleep(1000);           
        }        
    }
}

 

在這個程式碼中 Main 執行緒會等待 manger 睡眠結束後才會繼續執行,期間一直處於阻塞狀態。

還有一點很有趣,join 本質是讓當前執行緒呼叫該物件的 wait 方法,比如上文程式碼,本質是 Main 執行緒呼叫 managerwait 方法,再此物件上等待,而該被等待的執行緒結束後,會呼叫 notifyAll 方法告訴所有被等待的執行緒結束等待。所以最好不要線上程上呼叫 wait 方法,因為可能被 joinnotifyAll 意外喚醒。

最後思考一下 jionwait notify 之間的使用場景是很有意思的,wait notify 是 執行緒呼叫物件(由於join的存在,這個物件不能是執行緒!)的方法來進行協作,一個執行緒呼叫wait進入阻塞,另一個執行緒呼叫notify方法喚醒,一共三個物件(兩個執行緒,一個協作物件)。而 jion 的場景則是 一個執行緒呼叫 wait 方法等待一個執行緒,該執行緒呼叫notifyAll 方法喚醒該執行緒,沒有第三者。所以 jion 可以理解為兩個執行緒的互相協作,而 wait notify 是兩個執行緒通過一個物件進行協作,當然只是可以這樣理解,具體本質還需要好好在實踐生活中使用才能慢慢領會到。

 

相關文章