深入淺出Java多執行緒

程式設計師歐陽思海發表於2018-05-22

初遇

Java給多執行緒程式設計提供了內建的支援。一個多執行緒程式包含兩個或多個能併發執行的部分。程式的每一部分都稱作一個執行緒,並且每個執行緒定義了一個獨立的執行路徑。

多執行緒是多工的一種特別的形式,但多執行緒使用了更小的資源開銷。

這裡定義和執行緒相關的另一個術語 - 程式:一個程式包括由作業系統分配的記憶體空間,包含一個或多個執行緒。一個執行緒不能獨立的存在,它必須是程式的一部分。一個程式一直執行,直到所有的非守候執行緒都結束執行後才能結束。

多執行緒能滿足程式設計師編寫高效率的程式來達到充分利用CPU的目的。

1. 多執行緒基礎概念介紹

程式是程式(任務)的執行過程,它持有資源(共享記憶體,共享檔案)和執行緒

分析:

執行過程 是動態性的,你放在電腦磁碟上的某個eclipse或者QQ檔案並不是我們的程式,只有當你雙擊執行可執行檔案,使eclipse或者QQ執行之後,這才稱為程式。它是一個執行過程,是一個動態的概念。

它持有資源(共享記憶體,共享檔案)和執行緒:我們說程式是資源的載體,也是執行緒的載體。這裡的資源可以理解為記憶體。我們知道程式是要從記憶體中讀取資料進行執行的,所以每個程式獲得執行的時候會被分配一個記憶體。

③ 執行緒是什麼?

這裡寫圖片描述

如果我們把程式比作一個班級,那麼班級中的每個學生可以將它視作一個執行緒。學生是班級中的最小單元,構成了班級中的最小單位。一個班級有可以多個學生,這些學生都使用共同的桌椅、書籍以及黑板等等進行學習和生活。

在這個意義上我們說:

執行緒是系統中最小的執行單元;同一程式中可以有多個執行緒;執行緒共享程式的資源。

④ 執行緒是如何互動?

就如同一個班級中的多個學生一樣,我們說多個執行緒需要通訊才能正確的工作,這種通訊,我們稱作執行緒的互動

互動的方式:互斥、同步

類比班級,就是在同一班級之內,同學之間通過相互的協作才能完成某些任務,有時這種協作是需要競爭的,比如學習,班級之內公共的學習資料是有限的,愛學習的同學需要搶佔它,需要競爭,當一個同學使用完了之後另一個同學才可以使用;如果一個同學正在使用,那麼其他新來的同學只能等待;另一方面需要同步協作,就好比班級六一需要排演節目,同學需要齊心協力相互配合才能將節目演好,這就是程式互動。

一個執行緒的生命週期

執行緒經過其生命週期的各個階段。下圖顯示了一個執行緒完整的生命週期。

這裡寫圖片描述

  • 新建狀態:

使用 new 關鍵字和 Thread 類或其子類建立一個執行緒物件後,該執行緒物件就處於新建狀態。它保持這個狀態直到程式 start() 這個執行緒。

  • 就緒狀態:

當執行緒物件呼叫了start()方法之後,該執行緒就進入就緒狀態。就緒狀態的執行緒處於就緒佇列中,要等待JVM裡執行緒排程器的排程。

  • 執行狀態:

    如果就緒狀態的執行緒獲取 CPU 資源,就可以執行 run(),此時執行緒便處於執行狀態。處於執行狀態的執行緒最為複雜,它可以變為阻塞狀態、就緒狀態和死亡狀態。

  • 阻塞狀態:

如果一個執行緒執行了sleep(睡眠)、suspend(掛起)等方法,失去所佔用資源之後,該執行緒就從執行狀態進入阻塞狀態。在睡眠時間已到或獲得裝置資源後可以重新進入就緒狀態。

  • 死亡狀態:

一個執行狀態的執行緒完成任務或者其他終止條件發生時,該執行緒就切換到終止狀態。

執行緒的狀態轉換圖

這裡寫圖片描述

1、新建狀態(New):新建立了一個執行緒物件。

2、就緒狀態(Runnable):執行緒物件建立後,其他執行緒呼叫了該物件的start()方法。該狀態的執行緒位於可執行執行緒池中,變得可執行,等待獲取CPU的使用權。

3、執行狀態(Running):就緒狀態的執行緒獲取了CPU,執行程式程式碼。

4、阻塞狀態(Blocked):阻塞狀態是執行緒因為某種原因放棄CPU使用權,暫時停止執行。直到執行緒進入就緒狀態,才有機會轉到執行狀態。阻塞的情況分三種:

(一)、等待阻塞:執行的執行緒執行wait()方法,JVM會把該執行緒放入等待池中。

(二)、同步阻塞:執行的執行緒在獲取物件的同步鎖時,若該同步鎖被別的執行緒佔用,則JVM會把該執行緒放入鎖池中。

(三)、其他阻塞:執行的執行緒執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該執行緒置為阻塞狀態。當sleep()狀態超時、join()等待執行緒終止或者超時、或者I/O處理完畢時,執行緒重新轉入就緒狀態。

5、死亡狀態(Dead):執行緒執行完了或者因異常退出了run()方法,該執行緒結束生命週期。

執行緒的排程

1、調整執行緒優先順序:

每一個Java執行緒都有一個優先順序,這樣有助於作業系統確定執行緒的排程順序。

Java執行緒的優先順序用整數表示,取值範圍是1~10,Thread類有以下三個靜態常量: static int MAX_PRIORITY 執行緒可以具有的最高優先順序,取值為10。 static int MIN_PRIORITY 執行緒可以具有的最低優先順序,取值為1。 static int NORM_PRIORITY 分配給執行緒的預設優先順序,取值為5。

Thread類的setPriority()和getPriority()方法分別用來設定和獲取執行緒的優先順序。 每個執行緒都有預設的優先順序。主執行緒的預設優先順序為Thread.NORM_PRIORITY。 執行緒的優先順序有繼承關係,比如A執行緒中建立了B執行緒,那麼B將和A具有相同的優先順序。 JVM提供了10個執行緒優先順序,但與常見的作業系統都不能很好的對映。如果希望程式能移植到各個作業系統中,應該僅僅使用Thread類有以下三個靜態常量作為優先順序,這樣能保證同樣的優先順序採用了同樣的排程方式。

具有較高優先順序的執行緒對程式更重要,並且應該在低優先順序的執行緒之前分配處理器資源。但是,執行緒優先順序不能保證執行緒執行的順序,而且非常依賴於平臺。

2、執行緒睡眠:Thread.sleep(long millis)方法,使執行緒轉到阻塞狀態。millis引數設定睡眠的時間,以毫秒為單位。當睡眠結束後,就轉為就緒(Runnable)狀態。sleep()平臺移植性好。

3、執行緒等待:Object類中的wait()方法,導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 喚醒方法。這個兩個喚醒方法也是Object類中的方法,行為等價於呼叫 wait(0) 一樣。

4、執行緒讓步:Thread.yield() 方法,暫停當前正在執行的執行緒物件,把執行機會讓給相同或者更高優先順序的執行緒。

5、執行緒加入:join()方法,等待其他執行緒終止。在當前執行緒中呼叫另一個執行緒的join()方法,則當前執行緒轉入阻塞狀態,直到另一個程式執行結束,當前執行緒再由阻塞轉為就緒狀態。

6、執行緒喚醒:Object類中的notify()方法,喚醒在此物件監視器上等待的單個執行緒。如果所有執行緒都在此物件上等待,則會選擇喚醒其中一個執行緒。選擇是任意性的,並在對實現做出決定時發生。執行緒通過呼叫其中一個 wait 方法,在物件的監視器上等待。 直到當前的執行緒放棄此物件上的鎖定,才能繼續執行被喚醒的執行緒。被喚醒的執行緒將以常規方式與在該物件上主動同步的其他所有執行緒進行競爭;例如,喚醒的執行緒在作為鎖定此物件的下一個執行緒方面沒有可靠的特權或劣勢。類似的方法還有一個notifyAll(),喚醒在此物件監視器上等待的所有執行緒。

注意:Thread中suspend()和resume()兩個方法在JDK1.5中已經廢除,不再介紹。因為有死鎖傾向。

7、常見執行緒名詞解釋 主執行緒:JVM呼叫程式main()所產生的執行緒。 當前執行緒:這個是容易混淆的概念。一般指通過Thread.currentThread()來獲取的程式。 後臺執行緒:指為其他執行緒提供服務的執行緒,也稱為守護執行緒。JVM的垃圾回收執行緒就是一個後臺執行緒。 前臺執行緒:是指接受後臺執行緒服務的執行緒,其實前臺後臺執行緒是聯絡在一起,就像傀儡和幕後操縱者一樣的關係。傀儡是前臺執行緒、幕後操縱者是後臺執行緒。由前臺執行緒建立的執行緒預設也是前臺執行緒。可以通過isDaemon()和setDaemon()方法來判斷和設定一個執行緒是否為後臺執行緒。

一些常見問題

1、執行緒的名字,一個執行中的執行緒總是有名字的,名字有兩個來源,一個是虛擬機器自己給的名字,一個是你自己的定的名字。在沒有指定執行緒名字的情況下,虛擬機器總會為執行緒指定名字,並且主執行緒的名字總是main,非主執行緒的名字不確定。

2、執行緒都可以設定名字,也可以獲取執行緒的名字,連主執行緒也不例外。

3、獲取當前執行緒的物件的方法是:Thread.currentThread();

4、每個執行緒都將啟動,每個執行緒都將執行直到完成。一系列執行緒以某種順序啟動並不意味著將按該順序執行。對於任何一組啟動的執行緒來說,排程程式不能保證其執行次序,持續時間也無法保證。

5、當執行緒目標run()方法結束時該執行緒完成。

6、一旦執行緒啟動,它就永遠不能再重新啟動。只有一個新的執行緒可以被啟動,並且只能一次。一個可執行的執行緒或死執行緒可以被重新啟動。

7、執行緒的排程是JVM的一部分,在一個CPU的機器上上,實際上一次只能執行一個執行緒。一次只有一個執行緒棧執行。JVM執行緒排程程式決定實際執行哪個處於可執行狀態的執行緒。 眾多可執行執行緒中的某一個會被選中做為當前執行緒。可執行執行緒被選擇執行的順序是沒有保障的。

8、儘管通常採用佇列形式,但這是沒有保障的。佇列形式是指當一個執行緒完成“一輪”時,它移到可執行佇列的尾部等待,直到它最終排隊到該佇列的前端為止,它才能被再次選中。事實上,我們把它稱為可執行池而不是一個可執行佇列,目的是幫助認識執行緒並不都是以某種有保障的順序排列唱呢個一個佇列的事實。

9、儘管我們沒有無法控制執行緒排程程式,但可以通過別的方式來影響執行緒排程的方式。

2. Java 中執行緒的常用方法介紹

Java語言對執行緒的支援

主要體現在Thread類Runnable介面上,都繼承於java.lang包。它們都有個共同的方法:public void run()

  run方法為我們提供了執行緒實際工作執行的程式碼。

下表列出了Thread類的一些重要方法:

序號 方法描述
1 public void start()使該執行緒開始執行;Java 虛擬機器呼叫該執行緒的 run 方法。
2 public void run()如果該執行緒是使用獨立的 Runnable 執行物件構造的,則呼叫該 Runnable 物件的 run 方法;否則,該方法不執行任何操作並返回。
3 public final void setName(String name)改變執行緒名稱,使之與引數 name 相同。
4 public final void setPriority(int priority)更改執行緒的優先順序。
5 public final void setDaemon(boolean on)將該執行緒標記為守護執行緒或使用者執行緒。
6 public final void join(long millisec)等待該執行緒終止的時間最長為 millis 毫秒。
7 public void interrupt()中斷執行緒。
8 public final boolean isAlive()測試執行緒是否處於活動狀態。

測試執行緒是否處於活動狀態。 上述方法是被Thread物件呼叫的。下面的方法是Thread類的靜態方法。

序號 方法描述
1 public static void yield()暫停當前正在執行的執行緒物件,並執行其他執行緒。
2 public static void sleep(long millisec)在指定的毫秒數內讓當前正在執行的執行緒休眠(暫停執行),此操作受到系統計時器和排程程式精度和準確性的影響。
3 public static boolean holdsLock(Object x)當且僅當當前執行緒在指定的物件上保持監視器鎖時,才返回 true。
4 public static Thread currentThread()返回對當前正在執行的執行緒物件的引用。
5 public static void dumpStack()將當前執行緒的堆疊跟蹤列印至標準錯誤流。

Thread常用的方法

這裡寫圖片描述

3. 執行緒初體驗(編碼示例)

建立執行緒的方法有兩種:

1.繼承Thread類本身

2.實現Runnable介面

執行緒中的方法比較有特點,比如:啟動(start),休眠(sleep),停止等,多個執行緒是互動執行的(cpu在某個時刻。只能執行一個執行緒,當一個執行緒休眠了或者執行完畢了,另一個執行緒才能佔用cpu來執行)因為這是cpu的結構來決定的,在某個時刻cpu只能執行一個執行緒,不過速度相當快,對於人來將可以認為是並行執行的。

在一個java檔案中,可以有多個類(此處說的是外部類),但只能有一個public類。

這兩種建立執行緒的方法本質沒有任何的不同,一個是實現Runnable介面,一個是繼承Thread類。

使用實現Runnable介面這種方法:

  1.可以避免java的單繼承的特性帶來的侷限性;

  2.適合多個相同程式的程式碼去處理同一個資源情況,把執行緒同程式的程式碼及資料有效的分離,較好的體現了物件導向的設計思想。開發中大多數情況下都使用實現Runnable介面這種方法建立執行緒。

實現Runnable介面建立的執行緒最終還是要通過將自身例項作為引數傳遞給Thread然後執行

語法: Thread actress=new Thread(Runnable target ,String name);

例如:

Thread actressThread=new Thread(new Actress(),"Ms.runnable");
actressThread.start();
複製程式碼

程式碼示例:

package com.study.thread;

public class Actor extends Thread{
    public void run() {
        System.out.println(getName() + "是一個演員!");
        int count = 0;
        boolean keepRunning = true;

        while(keepRunning){
            System.out.println(getName()+"登臺演出:"+ (++count));
            if(count == 100){
                keepRunning = false;
            }
            if(count%10== 0){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println(getName() + "的演出結束了!");
    }

    public static void main(String[] args) {
       Thread actor = new Actor();//向上轉型:子類轉型為父類,子類物件就會遺失和父類不同的方法。向上轉型符合Java提倡的面向抽象程式設計思想,還可以減輕程式設計工作量
       actor.setName("Mr. Thread");
       actor.start();
       
       //呼叫Thread的建構函式Thread(Runnable target, String name)
       Thread actressThread = new Thread(new Actress(), "Ms. Runnable");
       actressThread.start();
    }

}
//注意:在“xx.java”檔案中可以有多個類,但是隻能有一個Public類。這裡所說的不是內部類,都是一個個獨立的外部類
class Actress implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "是一個演員!");//Runnable沒有getName()方法,需要通過執行緒的currentThread()方法獲得執行緒名稱
        int count = 0;
        boolean keepRunning = true;

        while(keepRunning){
            System.out.println(Thread.currentThread().getName()+"登臺演出:"+ (++count));
            if(count == 100){
                keepRunning = false;
            }
            if(count%10== 0){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println(Thread.currentThread().getName() + "的演出結束了!");
    }
    
}

/**
 *執行結果Mr. Thread執行緒和Ms. Runnable執行緒是交替執行的情況
 *分析:計算機CPU處理器在同一時間同一個處理器同一個核只能執行一條執行緒,
 *當一條執行緒休眠之後,另外一個執行緒才獲得處理器時間
 */
複製程式碼

執行結果:

這裡寫圖片描述
示例2:

ArmyRunnable 類:

package com.study.threadTest1;

/**
 * 軍隊執行緒
 * 模擬作戰雙方的行為
 */
public class ArmyRunnable implements Runnable {

    /* volatile關鍵字
     * volatile保證了執行緒可以正確的讀取其他執行緒寫入的值
     * 如果不寫成volatile,由於可見性的問題,當前執行緒有可能不能讀到這個值
     * 關於可見性的問題可以參考JMM(Java記憶體模型),裡面講述了:happens-before原則、可見性
     * 用volatile修飾的變數,執行緒在每次使用變數的時候,都會讀取變數修改後的值
     */
    volatile boolean keepRunning = true;

    @Override
    public void run() {
        while (keepRunning) {
            //發動5連擊
            for(int i=0;i<5;i++){
                System.out.println(Thread.currentThread().getName()+"進攻對方["+i+"]");
                //讓出了處理器時間,下次該誰進攻還不一定呢!
                Thread.yield();//yield()當前執行執行緒釋放處理器資源
            } 
        }
        System.out.println(Thread.currentThread().getName()+"結束了戰鬥!");
    }

}
複製程式碼

KeyPersonThread 類:

package com.study.threadTest1;


public class KeyPersonThread extends Thread {
    public void run(){
        System.out.println(Thread.currentThread().getName()+"開始了戰鬥!");
        for(int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"左突右殺,攻擊隋軍...");
        }
        System.out.println(Thread.currentThread().getName()+"結束了戰鬥!");
    }

}
複製程式碼

Stage 類:

package com.study.threadTest1;

/**
 * 隋唐演義大戲舞臺 6  */
public class Stage extends Thread {
    public void run(){
        System.out.println("歡迎觀看隋唐演義");
        //讓觀眾們安靜片刻,等待大戲上演
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        System.out.println("大幕徐徐拉開");

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }

        System.out.println("話說隋朝末年,隋軍與農民起義軍殺得昏天黑地...");
        ArmyRunnable armyTaskOfSuiDynasty = new ArmyRunnable();
        ArmyRunnable armyTaskOfRevolt = new ArmyRunnable();

        //使用Runnable介面建立執行緒
        Thread  armyOfSuiDynasty = new Thread(armyTaskOfSuiDynasty,"隋軍");
        Thread  armyOfRevolt = new Thread(armyTaskOfRevolt,"農民起義軍");

        //啟動執行緒,讓軍隊開始作戰
        armyOfSuiDynasty.start();
        armyOfRevolt.start();

        //舞臺執行緒休眠,大家專心觀看軍隊廝殺
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("正當雙方激戰正酣,半路殺出了個程咬金");

        Thread  mrCheng = new KeyPersonThread();
        mrCheng.setName("程咬金");
        System.out.println("程咬金的理想就是結束戰爭,使百姓安居樂業!");

        //停止軍隊作戰
        //停止執行緒的方法
        armyTaskOfSuiDynasty.keepRunning = false;
        armyTaskOfRevolt.keepRunning = false;

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        /*
         * 歷史大戲留給關鍵人物
         */
        mrCheng.start();

        //萬眾矚目,所有執行緒等待程先生完成歷史使命
        try {
            mrCheng.join();//join()使其他執行緒等待當前執行緒終止
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("戰爭結束,人民安居樂業,程先生實現了積極的人生夢想,為人民作出了貢獻!");
        System.out.println("謝謝觀看隋唐演義,再見!");
    }

    public static void main(String[] args) {
        new Stage().start();
    }

}
複製程式碼

執行結果:

這裡寫圖片描述

4. Java 執行緒的正確停止

如何正確的停止Java中的執行緒?

stop方法:該方法使執行緒戛然而止(突然停止),完成了哪些工作,哪些工作還沒有做都不清楚,且清理工作也沒有做。

stop方法不是正確的停止執行緒方法。執行緒停止不推薦使用stop方法。

正確的方法---設定退出標誌

使用volatile 定義boolean running=true,通過設定標誌變數running,來結束執行緒。

如本文:volatile boolean keepRunning=true;

這樣做的好處是:使得執行緒有機會使得一個完整的業務步驟被完整地執行,在執行完業務步驟後有充分的時間去做程式碼的清理工作,使得執行緒程式碼在實際中更安全。

這裡寫圖片描述

廣為流傳的錯誤方法---interrupt方法

這裡寫圖片描述
當一個執行緒執行時,另一個執行緒可以呼叫對應的 Thread 物件的 interrupt()方法來中斷它,該方法只是在目標執行緒中設定一個標誌,表示它已經被中斷,並立即返回。這裡需要注意的是,如果只是單純的呼叫 interrupt()方法,執行緒並沒有實際被中斷,會繼續往下執行。

程式碼示例:

package com.study.threadStop;

/**
 * 錯誤終止程式的方式——interrupt
 */
public class WrongWayStopThread extends Thread {

    public static void main(String[] args) {
        WrongWayStopThread thread = new WrongWayStopThread();
        System.out.println("Start Thread...");
        thread.start();
        
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("Interrupting thread...");
        thread.interrupt();
        
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Stopping application...");
    }
    
    public void run() {
        while(true){
            System.out.println("Thread is running...");
            long time = System.currentTimeMillis();
            while ((System.currentTimeMillis()-time) <1000) {//這部分的作用大致相當於Thread.sleep(1000),注意此處為什麼沒有使用休眠的方法
                //減少螢幕輸出的空迴圈(使得每秒鐘只輸出一行資訊)
            }
        }
    }
}
複製程式碼

執行結果:

這裡寫圖片描述
由結果看到interrupt()方法並沒有使執行緒中斷,執行緒還是會繼續往下執行。

Java API中介紹:

這裡寫圖片描述
但是interrupt()方法可以使我們的中斷狀態發生改變,可以呼叫isInterrupted 方法
這裡寫圖片描述
將上處run方法程式碼改為下面一樣,程式就可以正常結束了。

public void run() {
        while(!this.isInterrupted()){//interrupt()可以使中斷狀態放生改變,呼叫isInterrupted()
            System.out.println("Thread is running...");
            long time = System.currentTimeMillis();
            while ((System.currentTimeMillis()-time) <1000) {//這部分的作用大致相當於Thread.sleep(1000),注意此處為什麼沒有使用休眠的方法
                //減少螢幕輸出的空迴圈(使得每秒鐘只輸出一行資訊)
            }
        }
    }
複製程式碼

但是這種所使用的退出方法實質上還是前面說的使用退出旗標的方法,不過這裡所使用的退出旗標是一個特殊的標誌“執行緒是否被中斷的狀態”。

這裡寫圖片描述
這部分程式碼相當於執行緒休眠1秒鐘的程式碼。但是為什麼沒有使用Thread.sleep(1000)。如果採用這種方法就會出現
這裡寫圖片描述
執行緒沒有正常結束,而且還丟擲了一個異常,異常丟擲位置在呼叫interrupt方法之後。為什麼會有這種結果?

在API文件中說過:如果執行緒由於呼叫的某些方法(比如sleep,join。。。)而進入一種阻塞狀態時,此時如果這個執行緒再被呼叫interrupt方法,它會產生兩個結果:第一,它的中斷狀態被清除clear,而不是被設定set。那isInterrupted 就不能返回是否被中斷的正確狀態,那while函式就不能正確的退出。第二,sleep方法會收到InterruptedException被中斷。

interrupt()方法只能設定interrupt標誌位(且線上程阻塞情況下,標誌位會被清除,更無法設定中斷標誌位),無法停止執行緒

5. 執行緒互動

爭用條件:

1、當多個執行緒同時共享訪問同一資料(記憶體區域)時,每個執行緒都嘗試操作該資料,從而導致資料被破壞(corrupted),這種現象稱為爭用條件

2、原因是,每個執行緒在運算元據時,會先將資料初值讀【取到自己獲得的記憶體中】,然後在記憶體中進行運算後,重新賦值到資料。

3、爭用條件:執行緒1在還【未重新將值賦回去時】,執行緒1阻塞,執行緒2開始訪問該資料,然後進行了修改,之後被阻塞的執行緒1再獲得資源,而將之前計算的值覆蓋掉執行緒2所修改的值,就出現了資料丟失情況。

互斥與同步:守恆的能量

1、執行緒的特點,共享同一程式的資源,同一時刻只能有一個執行緒佔用CPU

2、由於執行緒有如上的特點,所以就會存在多個執行緒爭搶資源的現象,就會存在爭用條件這種現象

3、為了讓執行緒能夠正確的執行,不破壞共享的資料,所以,就產生了同步和互斥的兩種執行緒執行的機制

4、執行緒的互斥(加鎖實現):執行緒的執行隔離開來,互不影響,使用synchronized關鍵字實現互斥行為,此關鍵字即可以出現在方法體之上也可以出現在方法體內,以一種塊的形式出現,在此程式碼塊中有執行緒的等待和喚醒動作,用於支援執行緒的同步控制

5、執行緒的同步(執行緒的等待和喚醒:wait()+notifyAll()):執行緒的執行有相互的通訊控制,執行完一個再正確的執行另一個

6、鎖的概念:比如private final Object lockObj=new Object();

7、互斥實現方式:synchronized關鍵字

synchronized(lockObj){---執行程式碼----}加鎖操作

lockObj.wait();執行緒進入等待狀態,以避免執行緒持續申請鎖,而不去競爭cpu資源

lockObj.notifyAll();喚醒所有lockObj物件上等待的執行緒

8、加鎖操作會開銷系統資源,降低效率

同步問題提出

執行緒的同步是為了防止多個執行緒訪問一個資料物件時,對資料造成的破壞。 例如:兩個執行緒ThreadA、ThreadB都操作同一個物件Foo物件,並修改Foo物件上的資料。

public class Foo { 
    private int x = 100; 

    public int getX() { 
        return x; 
    } 

    public int fix(int y) { 
        x = x - y; 
        return x; 
    } 
}
複製程式碼
public class MyRunnable implements Runnable { 
    private Foo foo = new Foo(); 

    public static void main(String[] args) { 
        MyRunnable r = new MyRunnable(); 
        Thread ta = new Thread(r, "Thread-A"); 
        Thread tb = new Thread(r, "Thread-B"); 
        ta.start(); 
        tb.start(); 
    } 

    public void run() { 
        for (int i = 0; i < 3; i++) { 
            this.fix(30); 
            try { 
                Thread.sleep(1); 
            } catch (InterruptedException e) { 
                e.printStackTrace(); 
            } 
            System.out.println(Thread.currentThread().getName() + " : 當前foo物件的x值= " + foo.getX()); 
        } 
    } 

    public int fix(int y) { 
        return foo.fix(y); 
    } 
}
複製程式碼

執行結果:

Thread-A : 當前foo物件的x值= 40 
Thread-B : 當前foo物件的x值= 40 
Thread-B : 當前foo物件的x值= -20 
Thread-A : 當前foo物件的x值= -50 
Thread-A : 當前foo物件的x值= -80 
Thread-B : 當前foo物件的x值= -80 

Process finished with exit code 0
複製程式碼

從結果發現,這樣的輸出值明顯是不合理的。原因是兩個執行緒不加控制的訪問Foo物件並修改其資料所致。

如果要保持結果的合理性,只需要達到一個目的,就是將對Foo的訪問加以限制,每次只能有一個執行緒在訪問。這樣就能保證Foo物件中資料的合理性了。

在具體的Java程式碼中需要完成一下兩個操作: 把競爭訪問的資源類Foo變數x標識為private; 同步哪些修改變數的程式碼,使用synchronized關鍵字同步方法或程式碼。

同步和鎖定

1、鎖的原理

Java中每個物件都有一個內建鎖 當程式執行到非靜態的synchronized同步方法上時,自動獲得與正在執行程式碼類的當前例項(this例項)有關的鎖。獲得一個物件的鎖也稱為獲取鎖、鎖定物件、在物件上鎖定或在物件上同步。 當程式執行到synchronized同步方法或程式碼塊時才該物件鎖才起作用。 一個物件只有一個鎖。所以,如果一個執行緒獲得該鎖,就沒有其他執行緒可以獲得鎖,直到第一個執行緒釋放(或返回)鎖。這也意味著任何其他執行緒都不能進入該物件上的synchronized方法或程式碼塊,直到該鎖被釋放。 釋放鎖是指持鎖執行緒退出了synchronized同步方法或程式碼塊。

關於鎖和同步,有一下幾個要點:

1)、只能同步方法,而不能同步變數和類;

2)、每個物件只有一個鎖;當提到同步時,應該清楚在什麼上同步?也就是說,在哪個物件上同步?

3)、不必同步類中所有的方法,類可以同時擁有同步和非同步方法。

4)、如果兩個執行緒要執行一個類中的synchronized方法,並且兩個執行緒使用相同的例項來呼叫方法,那麼一次只能有一個執行緒能夠執行方法,另一個需要等待,直到鎖被釋放。也就是說:如果一個執行緒在物件上獲得一個鎖,就沒有任何其他執行緒可以進入(該物件的)類中的任何一個同步方法。

5)、如果執行緒擁有同步和非同步方法,則非同步方法可以被多個執行緒自由訪問而不受鎖的限制。

6)、執行緒睡眠時,它所持的任何鎖都不會釋放。

7)、執行緒可以獲得多個鎖。比如,在一個物件的同步方法裡面呼叫另外一個物件的同步方法,則獲取了兩個物件的同步鎖。

8)、同步損害併發性,應該儘可能縮小同步範圍。同步不但可以同步整個方法,還可以同步方法中一部分程式碼塊。

9)、在使用同步程式碼塊時候,應該指定在哪個物件上同步,也就是說要獲取哪個物件的鎖。例如:

public int fix(int y) {
        synchronized (this) {
            x = x - y;
        }
        return x;
    }
複製程式碼

當然,同步方法也可以改寫為非同步方法,但功能完全一樣的,例如:

 public synchronized int getX() {
        return x++;
    }
複製程式碼

 public int getX() {
        synchronized (this) {
            return x;
        }
    }
複製程式碼

效果是完全一樣的。

靜態方法同步

要同步靜態方法,需要一個用於整個類物件的鎖,這個物件是就是這個類(XXX.class)。 例如:

public static synchronized int setName(String name){
      Xxx.name = name;
}
複製程式碼

等價於

public static int setName(String name){
      synchronized(Xxx.class){
            Xxx.name = name;
      }
}
複製程式碼

執行緒同步小結

1、執行緒同步的目的是為了保護多個執行緒訪問一個資源時對資源的破壞。

2、執行緒同步方法是通過鎖來實現,每個物件都有切僅有一個鎖,這個鎖與一個特定的物件關聯,執行緒一旦獲取了物件鎖,其他訪問該物件的執行緒就無法再訪問該物件的其他同步方法。

3、對於靜態同步方法,鎖是針對這個類的,鎖物件是該類的Class物件。靜態和非靜態方法的鎖互不干預。一個執行緒獲得鎖,當在一個同步方法中訪問另外物件上的同步方法時,會獲取這兩個物件鎖。

4、對於同步,要時刻清醒在哪個物件上同步,這是關鍵。

5、編寫執行緒安全的類,需要時刻注意對多個執行緒競爭訪問資源的邏輯和安全做出正確的判斷,對“原子”操作做出分析,並保證原子操作期間別的執行緒無法訪問競爭資源。

6、當多個執行緒等待一個物件鎖時,沒有獲取到鎖的執行緒將發生阻塞。

7、死鎖是執行緒間相互等待鎖鎖造成的,在實際中發生的概率非常的小。真讓你寫個死鎖程式,不一定好使,呵呵。但是,一旦程式發生死鎖,程式將死掉。

深入剖析互斥與同步

互斥的實現(加鎖):synchronized(lockObj); 保證的同一時間,只有一個執行緒獲得lockObj.

同步的實現:wait()/notify()/notifyAll()

注意: wait()、notify()、notifyAll()方法均屬於Object物件,而不是Thread物件。

  • void notify() 喚醒在此物件監視器上等待的單個執行緒。
  • void notifyAll() 喚醒在此物件監視器上等待的所有執行緒。
  • void wait() 導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 方法。

當然,wait()還有另外兩個過載方法:

  • void wait(long timeout) 導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 方法,或者超過指定的時間量。
  • void wait(long timeout, int nanos) 導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 方法,或者其他某個執行緒中斷當前執行緒,或者已超過某個實際時間量。

notify()喚醒wait set中的一條執行緒,而notifyall()喚醒所有執行緒。

同步是兩個執行緒之間的一種互動的操作(一個執行緒發出訊息另外一個執行緒響應) 關於等待/通知,要記住的關鍵點是: 必須從同步環境內呼叫wait()、notify()、notifyAll()方法。執行緒不能呼叫物件上等待或通知的方法,除非它擁有那個物件的鎖。 wait()、notify()、notifyAll()都是Object的例項方法。與每個物件具有鎖一樣,每個物件可以有一個執行緒列表,他們等待來自該訊號(通知)。執行緒通過執行物件上的wait()方法獲得這個等待列表。從那時候起,它不再執行任何其他指令,直到呼叫物件的notify()方法為止。如果多個執行緒在同一個物件上等待,則將只選擇一個執行緒(不保證以何種順序)繼續執行。如果沒有執行緒等待,則不採取任何特殊操作。 下面看個例子就明白了:

/** 
* 計算輸出其他執行緒鎖計算的資料 
*/ 
public class ThreadA { 
    public static void main(String[] args) { 
        ThreadB b = new ThreadB(); 
        //啟動計算執行緒 
        b.start(); 
        //執行緒A擁有b物件上的鎖。執行緒為了呼叫wait()或notify()方法,該執行緒必須是那個物件鎖的擁有者 
        synchronized (b) { 
            try { 
                System.out.println("等待物件b完成計算。。。"); 
                //當前執行緒A等待 
                b.wait(); 
            } catch (InterruptedException e) { 
                e.printStackTrace(); 
            } 
            System.out.println("b物件計算的總和是:" + b.total); 
        } 
    } 
}
複製程式碼
/** 
* 計算1+2+3 ... +100的和 
*/ 
public class ThreadB extends Thread { 
    int total; 

    public void run() { 
        synchronized (this) { 
            for (int i = 0; i < 101; i++) { 
                total += i; 
            } 
            //(完成計算了)喚醒在此物件監視器上等待的單個執行緒,在本例中執行緒A被喚醒 
            notify(); 
        } 
    } 
}
複製程式碼

結果: 等待物件b完成計算。。。 b物件計算的總和是:5050 Process finished with exit code 0

千萬注意: 當在物件上呼叫wait()方法時,執行該程式碼的執行緒立即放棄它在物件上的鎖。然而呼叫notify()時,並不意味著這時執行緒會放棄其鎖。如果執行緒榮然在完成同步程式碼,則執行緒在移出之前不會放棄鎖。因此,只要呼叫notify()並不意味著這時該鎖變得可用。

多個執行緒在等待一個物件鎖時候使用notifyAll(): 在多數情況下,最好通知等待某個物件的所有執行緒。如果這樣做,可以在物件上使用notifyAll()讓所有在此物件上等待的執行緒衝出等待區,返回到可執行狀態。

如何理解同步:Wait Set

Critical Section(臨界資源)Wait Set(等待區域)

wait set 類似於執行緒的休息室,訪問共享資料的程式碼稱為critical section。一個執行緒獲取鎖,然後進入臨界區,發現某些條件不滿足,然後呼叫鎖物件上的wait方法,然後執行緒釋放掉鎖資源,進入鎖物件上的wait set。由於執行緒釋放釋放了理解資源,其他執行緒可以獲取所資源,然後執行,完了以後呼叫notify,通知鎖物件上的等待執行緒。

Ps:若呼叫notify();則隨機拿出(這隨機拿出是內部的演算法,無需瞭解)一條在等待的資源進行準備進入Critical Section;若呼叫notifyAll();則全部取出進行準備進入Critical Section。

6. 總結與展望

這裡寫圖片描述
這裡寫圖片描述
擴充套件建議:如何擴充套件Java併發知識

1、Java Memory Mode : JMM描述了java執行緒如何通過記憶體進行互動,瞭解happens-before , synchronized,voliatile & final

2、Locks % Condition:Java鎖機制和等待條件的高層實現 java.util,concurrent.locks

3、執行緒安全性:原子性與可見性, java.util.concurrent.atomic synchronized(鎖的方法塊)&volatile(定義公共資源) DeadLocks(死鎖)--瞭解什麼是死鎖,死鎖產生的條件

4、多執行緒程式設計常用的互動模型

· Producer-Consumer模型(生產者-消費者模型)

· Read-Write Lock模型(讀寫鎖模型)

· Future模型

· Worker Thread模型

考慮在Java併發實現當中,有哪些類實現了這些模型,供我們直接呼叫

5、Java5中併發程式設計工具:java.util.concurrent 包下的

例如:執行緒池ExcutorService 、Callable&Future 、BlockingQueue

6、推薦書本:CoreJava 、JavaConcurrency In Practice

  • 出處:http://www.cnblogs.com/Qian123/p/5670304.html

文章有不當之處,歡迎指正,你也可以關注我的微信公眾號:好好學java,獲取優質學習資源。

相關文章