深刻淺出Java多線程

初遇

Java給多線程編程提供了內置的支持。一個多線程程序包含兩個或多個能併發運行的部分。程序的每一部分都稱做一個線程,而且每一個線程定義了一個獨立的執行路徑。html

多線程是多任務的一種特別的形式,但多線程使用了更小的資源開銷。前端

這裏定義和線程相關的另外一個術語 - 進程:一個進程包括由操做系統分配的內存空間,包含一個或多個線程。一個線程不能獨立的存在,它必須是進程的一部分。一個進程一直運行,直到全部的非守候線程都結束運行後才能結束。java

多線程能知足程序員編寫高效率的程序來達到充分利用CPU的目的。程序員

1. 多線程基礎概念介紹

進程是程序(任務)的執行過程,它持有資源(共享內存,共享文件)和線程算法

分析:編程

執行過程 是動態性的,你放在電腦磁盤上的某個eclipse或者QQ文件並非咱們的進程,只有當你雙擊運行可執行文件,使eclipse或者QQ運行以後,這才稱爲進程。它是一個執行過程,是一個動態的概念。安全

它持有資源(共享內存,共享文件)和線程:咱們說進程是資源的載體,也是線程的載體。這裏的資源能夠理解爲內存。咱們知道程序是要從內存中讀取數據進行運行的,因此每一個進程得到執行的時候會被分配一個內存。微信

③ 線程是什麼?
這裏寫圖片描述多線程

若是咱們把進程比做一個班級,那麼班級中的每一個學生能夠將它視做一個線程。學生是班級中的最小單元,構成了班級中的最小單位。一個班級有能夠多個學生,這些學生都使用共同的桌椅、書籍以及黑板等等進行學習和生活。併發

在這個意義上咱們說:

線程是系統中最小的執行單元;同一進程中能夠有多個線程;線程共享進程的資源。

④ 線程是如何交互?

就如同一個班級中的多個學生同樣,咱們說多個線程須要通訊才能正確的工做,這種通訊,咱們稱做線程的交互

交互的方式:互斥、同步

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

一個線程的生命週期

線程通過其生命週期的各個階段。下圖顯示了一個線程完整的生命週期。
這裏寫圖片描述

  • 新建狀態:

使用 new 關鍵字和 Thread 類或其子類創建一個線程對象後,該線程對象就處於新建狀態。它保持這個狀態直到程序 start() 這個線程。

  • 就緒狀態:

當線程對象調用了start()方法以後,該線程就進入就緒狀態。就緒狀態的線程處於就緒隊列中,要等待JVM裏線程調度器的調度。

  • 運行狀態:

    若是就緒狀態的線程獲取 CPU 資源,就能夠執行 run(),此時線程便處於運行狀態。處於運行狀態的線程最爲複雜,它能夠變爲阻塞狀態、就緒狀態和死亡狀態。

  • 阻塞狀態:

若是一個線程執行了sleep(睡眠)、suspend(掛起)等方法,失去所佔用資源以後,該線程就從運行狀態進入阻塞狀態。在睡眠時間已到或得到設備資源後能夠從新進入就緒狀態。

  • 死亡狀態:

一個運行狀態的線程完成任務或者其餘終止條件發生時,該線程就切換到終止狀態。

線程的狀態轉換圖

這裏寫圖片描述

一、新建狀態(New):新建立了一個線程對象。

二、就緒狀態(Runnable):線程對象建立後,其餘線程調用了該對象的start()方法。該狀態的線程位於可運行線程池中,變得可運行,等待獲取CPU的使用權。

三、運行狀態(Running):就緒狀態的線程獲取了CPU,執行程序代碼。

四、阻塞狀態(Blocked):阻塞狀態是線程由於某種緣由放棄CPU使用權,暫時中止運行。直到線程進入就緒狀態,纔有機會轉到運行狀態。阻塞的狀況分三種:

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

(二)、同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM會把該線程放入鎖池中。

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

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

線程的調度

一、調整線程優先級:

每個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類有如下三個靜態常量做爲優先級,這樣能保證一樣的優先級採用了一樣的調度方式。

具備較高優先級的線程對程序更重要,而且應該在低優先級的線程以前分配處理器資源。可是,線程優先級不能保證線程執行的順序,並且很是依賴於平臺。

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

三、線程等待:Object類中的wait()方法,致使當前的線程等待,直到其餘線程調用此對象的 notify() 方法或 notifyAll() 喚醒方法。這個兩個喚醒方法也是Object類中的方法,行爲等價於調用 wait(0) 同樣。

四、線程讓步:Thread.yield() 方法,暫停當前正在執行的線程對象,把執行機會讓給相同或者更高優先級的線程。

五、線程加入:join()方法,等待其餘線程終止。在當前線程中調用另外一個線程的join()方法,則當前線程轉入阻塞狀態,直到另外一個進程運行結束,當前線程再由阻塞轉爲就緒狀態。

六、線程喚醒:Object類中的notify()方法,喚醒在此對象監視器上等待的單個線程。若是全部線程都在此對象上等待,則會選擇喚醒其中一個線程。選擇是任意性的,並在對實現作出決定時發生。線程經過調用其中一個 wait 方法,在對象的監視器上等待。 直到當前的線程放棄此對象上的鎖定,才能繼續執行被喚醒的線程。被喚醒的線程將以常規方式與在該對象上主動同步的其餘全部線程進行競爭;例如,喚醒的線程在做爲鎖定此對象的下一個線程方面沒有可靠的特權或劣勢。相似的方法還有一個notifyAll(),喚醒在此對象監視器上等待的全部線程。

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

七、常見線程名詞解釋
主線程:JVM調用程序main()所產生的線程。
當前線程:這個是容易混淆的概念。通常指經過Thread.currentThread()來獲取的進程。
後臺線程:指爲其餘線程提供服務的線程,也稱爲守護線程。JVM的垃圾回收線程就是一個後臺線程。
前臺線程:是指接受後臺線程服務的線程,其實前臺後臺線程是聯繫在一塊兒,就像傀儡和幕後操縱者同樣的關係。傀儡是前臺線程、幕後操縱者是後臺線程。由前臺線程建立的線程默認也是前臺線程。能夠經過isDaemon()和setDaemon()方法來判斷和設置一個線程是否爲後臺線程。

一些常見問題

一、線程的名字,一個運行中的線程老是有名字的,名字有兩個來源,一個是虛擬機本身給的名字,一個是你本身的定的名字。在沒有指定線程名字的狀況下,虛擬機總會爲線程指定名字,而且主線程的名字老是main,非主線程的名字不肯定。

二、線程均可以設置名字,也能夠獲取線程的名字,連主線程也不例外。

三、獲取當前線程的對象的方法是:Thread.currentThread();

四、每一個線程都將啓動,每一個線程都將運行直到完成。一系列線程以某種順序啓動並不意味着將按該順序執行。對於任何一組啓動的線程來講,調度程序不能保證其執行次序,持續時間也沒法保證。

五、當線程目標run()方法結束時該線程完成。

六、一旦線程啓動,它就永遠不能再從新啓動。只有一個新的線程能夠被啓動,而且只能一次。一個可運行的線程或死線程能夠被從新啓動。

七、線程的調度是JVM的一部分,在一個CPU的機器上上,實際上一次只能運行一個線程。一次只有一個線程棧執行。JVM線程調度程序決定實際運行哪一個處於可運行狀態的線程。
衆多可運行線程中的某一個會被選中作爲當前線程。可運行線程被選擇運行的順序是沒有保障的。

八、儘管一般採用隊列形式,但這是沒有保障的。隊列形式是指當一個線程完成「一輪」時,它移到可運行隊列的尾部等待,直到它最終排隊到該隊列的前端爲止,它才能被再次選中。事實上,咱們把它稱爲可運行池而不是一個可運行隊列,目的是幫助認識線程並不都是以某種有保障的順序排列唱呢個一個隊列的事實。

九、儘管咱們沒有沒法控制線程調度程序,但能夠經過別的方式來影響線程調度的方式。

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. 線程交互

爭用條件:

一、當多個線程同時共享訪問同一數據(內存區域)時,每一個線程都嘗試操做該數據,從而致使數據被破壞(corrupted),這種現象稱爲爭用條件

二、緣由是,每一個線程在操做數據時,會先將數據初值讀【取到本身得到的內存中】,而後在內存中進行運算後,從新賦值到數據。

三、爭用條件:線程1在還【未從新將值賦回去時】,線程1阻塞,線程2開始訪問該數據,而後進行了修改,以後被阻塞的線程1再得到資源,而將以前計算的值覆蓋掉線程2所修改的值,就出現了數據丟失狀況。

互斥與同步:守恆的能量

一、線程的特色,共享同一進程的資源,同一時刻只能有一個線程佔用CPU

二、因爲線程有如上的特色,因此就會存在多個線程爭搶資源的現象,就會存在爭用條件這種現象

三、爲了讓線程可以正確的運行,不破壞共享的數據,因此,就產生了同步和互斥的兩種線程運行的機制

四、線程的互斥(加鎖實現):線程的運行隔離開來,互不影響,使用synchronized關鍵字實現互斥行爲,此關鍵字便可以出如今方法體之上也能夠出如今方法體內,以一種塊的形式出現,在此代碼塊中有線程的等待和喚醒動做,用於支持線程的同步控制

五、線程的同步(線程的等待和喚醒:wait()+notifyAll()):線程的運行有相互的通訊控制,運行完一個再正確的運行另外一個

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

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

synchronized(lockObj){---執行代碼----}加鎖操做

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

lockObj.notifyAll();喚醒全部lockObj對象上等待的線程

八、加鎖操做會開銷系統資源,下降效率

同步問題提出

線程的同步是爲了防止多個線程訪問一個數據對象時,對數據形成的破壞。
例如:兩個線程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關鍵字同步方法或代碼。

同步和鎖定

一、鎖的原理

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;
      }
}

線程同步小結

一、線程同步的目的是爲了保護多個線程訪問一個資源時對資源的破壞。

二、線程同步方法是經過鎖來實現,每一個對象都有切僅有一個鎖,這個鎖與一個特定的對象關聯,線程一旦獲取了對象鎖,其餘訪問該對象的線程就沒法再訪問該對象的其餘同步方法。

三、對於靜態同步方法,鎖是針對這個類的,鎖對象是該類的Class對象。靜態和非靜態方法的鎖互不干預。一個線程得到鎖,當在一個同步方法中訪問另外對象上的同步方法時,會獲取這兩個對象鎖。

四、對於同步,要時刻清醒在哪一個對象上同步,這是關鍵。

五、編寫線程安全的類,須要時刻注意對多個線程競爭訪問資源的邏輯和安全作出正確的判斷,對「原子」操做作出分析,並保證原子操做期間別的線程沒法訪問競爭資源。

六、當多個線程等待一個對象鎖時,沒有獲取到鎖的線程將發生阻塞。

七、死鎖是線程間相互等待鎖鎖形成的,在實際中發生的機率很是的小。真讓你寫個死鎖程序,不必定好使,呵呵。可是,一旦程序發生死鎖,程序將死掉。

深刻剖析互斥與同步

互斥的實現(加鎖):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併發知識

一、Java Memory Mode : JMM描述了java線程如何經過內存進行交互,瞭解happens-before , synchronized,voliatile & final

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

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

四、多線程編程經常使用的交互模型

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

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

· Future模型

· Worker Thread模型

考慮在Java併發實現當中,有哪些類實現了這些模型,供咱們直接調用

五、Java5中併發編程工具:java.util.concurrent 包下的

例如:線程池ExcutorService 、Callable&Future 、BlockingQueue

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

文章有不當之處,歡迎指正,你也能夠關注個人微信公衆號: 好好學java,獲取優質學習資源。
相關文章
相關標籤/搜索