Java並發實戰一:線程與線程安全


從零開始創建一家公司

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的。

為什么呢?如果這兩個方法是Thread的話兩個線程之間的協作就會收到極大的限制,而如果是object的話兩個線程可以通過任意一個對象進行通信。

具體過程如下 當一個線程調用等待方法時,它會加入一個等待隊列。由於有多個線程可能擁有該對象,當不同線程先后調用這個方法時,都會加入這個對象的等待隊列中。

當 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 是兩個線程通過一個對象進行協作,當然只是可以這樣理解,具體本質還需要好好在實踐生活中使用才能慢慢領會到。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM