深入淺出Java多線程

初遇

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章