Thread

Thread


大部分時候我們都做着單線程的編程,都只有==一條順序執行流==:程序從main方法開始執行,依次向下執行每行代碼,如果程序執行某行代碼遇到了阻塞,程序將會停滯在此處。

==單線程的程序只有一個順序執行流,多線程的程序則可以包括多個順序執行流,多個順序流之間互不干擾。==

可以這樣理解:單線程程序如果只有一個服務員的餐廳,他必須做完一件事之後纔可以做下一件事;多線程程序則如同有多個服務員的餐廳,他們可以同時進行着許多事情。



1.線程概述

一個操作系統裏可以有多個進程,而一個進程裏可以有多個線程。

進程

幾乎所有操作系統都支持==進程==的概念,所有運行中的任務通常對應這一條進程(Process)。當一個程序進入內存(存儲正在運行的程序和數據)運行時,就變成了一個進程。

進程是處於運行過程中的程序,並具有一定獨立功能,是系統進行資源分配和調度的一個獨立單位。

進程的特徵:

  • 獨立性

    進程是系統中獨立存在的實體,可以擁有自己獨立的資源,每個進程都有自己私有的地址空間(獨立的代碼和數據空間)。進程間的切換會有較大的開銷。

  • 動態性

    進程與程序的區別在於:程序只是一個==靜態==的指令集合,而進程是一個正在系統中活動==(動態)==的指令集合

    在進程中加入了時間的概念。進程具有自己的生命週期和各種不同的狀態,而這些概念在程序中都是不具備的。

  • 併發性(concurrency)

    多個進程可以在單個處理器上併發執行,多個線程之間不會互相影響。


    注意區分併發性(concurrency)和並行性(parallel)這兩個概念:

    • 並行(parallel):指在同一時刻,有==多條==指令在==多個處理器==上==同時==執行

    • 併發(Concurrency): 指在同一時刻,只能有==一==條指令執行,但多個進程指令被快速輪換執行,使得在宏觀上具有多個進程同時執行的效果

    舉個例子:現代的操作系統幾乎都支持同時運行多個任務:一邊開着ide寫代碼,一邊開着網頁在查API,同時還在聽音樂,用markdown作筆記…這些進程開上去像是在同時工作。

    但是真相是:==對於一個CPU而言,它在某一個時間點上只能執行一個程序,也就是說只能運行一個進程。==*CPU不斷的在這些進程之間快速輪換執行,那麼爲什麼感覺不到任何中斷現象呢?這是因爲CPU的執行速度相對我們的感覺來說實在是太快了。*如果啓動的程序(進程)足夠多,我們依然可以感覺程序的運行速度下降(電腦變卡)。



線程

多線程擴展了多進程的概念,使得同一個進程(注意這裏是限於一個進程裏!)可以同時併發處理多個任務。

==線程(Thread)==也被稱作 ==輕量級進程(Lightweight Process)==。線程(Thread)是進程(Process)的執行單元。就像進程在操作系統中的地位一樣,線程在程序中是獨立的併發的執行流。當進程被初始化之後,主線程就被創建了。對於大多數的應用程序來說,通常僅要求有一個主線程,但我們也可以在該進程內創建多條順序執行流,這些順序執行流就是Thread,每條Thread也是互相獨立的。

  • 線程是進程的組成部分,一個進程可以有多個線程,一個線程必須有一個父進程。

  • 一個線程可以擁有自己的堆、棧、自己的程序計數器(PC)和自己的局部變量,==但不再擁有系統資源,它與父進程的其他線程共享該進程(Process)所擁有的全部資源。==

  • 因爲多個線程共享父進程的全部資源,因此編程更加方便;但必須注意的是:==必須確保一個線程不會妨礙同一進程裏的其他線程!==

  • 線程可以完成一定的任務,可與其他線程共享父進程中的共享變量及部分環境,相互之間協同來完成進程所要完成的任務。

  • 線程是獨立運行的,==它並不知道進程中是否還有其他線程的存在==。線程的運行是==搶佔式==的 ———> 當前運行的線程在任何時候都可能被掛起,以便另一個線程可以運行。

  • 一個線程可以創建和撤銷另一個線程(例如在main方法這個主線程裏創建另一個線程),同一個進程(Process)的多個線程(Thread)之間可以併發執行(concurrency,多個線程之間快速切換,快到讓人感覺是在同時執行)

從邏輯角度來看:多線程存在於一個應用程序中,讓一個應用程序中可以有==多個執行部分同時==執行;但操作系統無需將多個線程看做多個獨立的應用,對多線程實現調度和管理、資源分配由==進程==本身負責完成。

總結:一個程序運行後至少有一個進程,一個進程裏可以包含多個線程,但至少要有一個線程(主線程)


多線程的優勢

  • 線程的劃分尺度小於進程,使得多線程程序的==併發性高==。進程在執行過程中擁有獨立的內存單元,而多個線程共享內存,從而極大的提高了程序的效率。

  • 線程比進程具有更高的性能,這是由於同一個進程中的線程都有共性:多個線程經共享同一個進程的虛擬空間。線程共享的環境包括:進程代碼段、進程的公有數據等。

  • 系統創建進程必須爲該進程分配獨立的內存空間,並分配大量相關資源,但創建一個線程則簡單的得多。




1.創建和啓動

java使用Thread類代表線程,所有的線程都必須是Thread類或其子類。

每條線程的作用是:==完成一定的任務,實際上就是執行一段程序流。==*java使用run方法來封裝這樣一段程序流,run方法也被成爲==線程執行體==*

1.1 繼承Thread類創建線程類

步驟:

  1. 定義Thread類的子類,重寫該類run方法。==run方法的方法體代表了該線程需要完成的任務==(想要它做什麼,就往run方法裏面寫什麼)。

  2. 創建線程對象

  3. 用線程對象的start方法來啓動線程。

例:

package thread.createThread;

public class FirstThread extends Thread {

    private int i;

    public void run() {
        for (; i < 20; i++) {
            // shiyong getName()方法來返回當前線程的名字
            System.out.println(this.getName() + " " + i);
        }
    }

    public static void main(String[] args) {

        for (int i = 0; i < 20; i++) {

            // 使用Thread類的靜態方法 currentThread() 來獲取當前線程
            System.out.println(Thread.currentThread().getName() 
                + " " + i);

            if (i == 10) {
                new FirstThread().start();
            }
            if (i == 15) {
                new FirstThread().start();
            }

        }
    }
}
  • 上面的程序只顯式的啓動了兩條線程,但實際上有==3條線程==,因爲還有包含main方法的主線程。主線程的線程體不是有run方法確定的,而是由main方法的方法體來確定。

  • 上面還用到了線程類的兩個方法:

    • Thread.currentThread():返回當前正在執行的線程對象

    • getName():返回調用該方法的線程的名字。

      程序可以通過setName(String name)方法來爲線程設置名字。默認情況下下主線程的名字爲main,用戶啓動的多條線程名字依次爲Thread-0, Thread-1…

  • 上面程序Thread-0和Thread-1輸出的i並不連續,這是因爲i是實例屬性,程序每次創建線程對象都需要創建一個FirstThread對象,Thread-0和Thread-1不能共享i。(但如果把i設成static就可以)

    使用繼承Thread類的方法來創建線程,多條線程之間無法共享實例變量。



1.2 實現Runnable接口創建線程類

步驟:

  1. 定義實現Runnable接口的類,重寫run方法

    public class SecondThread implements Runnable
  2. 創建Runnable實現類的對象,並以此作爲Thread的target來創建Thread對象,這個Thread對象纔是真正的線程對象。

    SecondThread st = new SecondThread();
    
    new Thread(st, "NewThread");

    Runnable對象僅僅作爲Thread對象的Target(在創建Thread對象時作爲參數傳進構造方法,Runnale實現類裏包含的run方法僅僅作爲線程執行體。==而實際的線程對象依然是Thread類的實例,只是該Thread線程負責執行其Target的run方法而已==

  3. 調用線程對象的start方法來啓動該線程

例:

package thread.createThread;

public class SecondThread implements Runnable {

    private int i;

    public void run() {
        for (; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() 
                + " " + i);
        }
    }

    public static void main(String[] args) {

        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() 
                + " " + i);
            SecondThread st = new SecondThread();
            if (i == 10) {
                new Thread(st, "newThread1").start();
            }
            if (i == 15) {
                new Thread(st, "newThread2").start();
            }

        }
    }

}
  • 這時兩個線程的i變量是連續的,因爲程序所創建的Runnable對象只是線程的target,而多條線程可以共享同一個target,也就是說可以共享同一個target的所有實例變量。


兩種方式對比

優缺點 繼承Thread 實現Runnable
優點 簡單,直接使用this.getName()來獲取當前線程(因爲本身是一個線程類對象) 1.只是實現了Runnable接口,還可以繼承其他類

2.多個線程共享一個target對象,非常適合多個線程來處理同一份資源的情況
缺點 因爲線程類已經繼承了Thread,所以不能再繼承其他父類了 略微複雜,要使用Thread.currentThread()來獲取當前線程

==幾乎所有的多線程應用都採用實現Runnable接口的方式。==

2.線程的生命週期

當線程被創建並啓動以後,它既不是一啓動就進入了執行狀態,也不是一直處於執行狀態。

在線程的生命週期中,它要經過

  1. 新建(new)
  2. 就緒(runnable)
  3. 運行(running)
  4. 阻塞(blocked)
  5. 死亡(dead)

五種狀態。尤其是線程啓動以後,它不能一直”霸佔“着CPU獨自運行,CPU需要在多條線程之間切換,所以線程狀態也會多次在運行、阻塞之間切換。


                                                       
   --start()-->   --獲得處理器資源-->   --run執行完--------->
                  <--失去處理器資源--   --Error/Exception-->
                                                      
                ^                   |
                |                   |
                |______ blcok ______| 


2.1 新建、就緒狀態

新建(new)

當程序使用new關鍵字創建了一個線程之後,該線程就處於==新建(new)==狀態,此時它和其他java對象一樣,僅僅由java虛擬機爲它分配了內存,並初始化了其成員變量的值。==(僅僅就是一個普通的對象)==

就緒(runnable)

當線程對象調用了start()方法之後,該線程處於==就緒(runnable)==狀態,java虛擬機會爲其創建方法調用棧和程序計數器,處於這個狀態的線程==並沒有開始==運行,只是表示該線程可以運行了。至於該線程何時開始運行,取決於JVM裏線程調度器的調度。

注意

  1. ==啓動線程使用start()方法,而不是run()方法!!!!==

    永遠不要調用線程對象的run()方法!!!!
    永遠不要調用線程對象的run()方法!!!!
    永遠不要調用線程對象的run()方法!!!!

    (重要的事情說三次。。。)

    • 調用start方法來啓動線程,系統會把該run方法當成線程執行體來處理。多個線程之間可以併發執行
    • 但是如果直接調用線程對象的run方法,run方法會被立刻執行,而且在run方法返回之前其他線程無法併發執行。(變成了普通的方法調用!!!
  2. 不要對已經啓動的線程再次調用start方法,否則會引發IllegalThreadStateException

  3. 線程調度器切換線程由底層平臺控制,具有一定隨機性

  4. 如果希望調用子線程的strat方法立刻執行子線程,可以使用Thread.sleep(1)來是當前運行的線程(主線程)睡眠一個毫秒,這一毫秒內CPU不會空閒,它會立刻去執行一條就緒的線程。

2.2 運行、阻塞

運行

處於就緒狀態(runnable)的線程獲得了CPU,開始執行run方法的線程執行體。

  • 如果計算機只有一個CPU,在任何時候==只有一條線程==處於運行狀態。在多CPU的機器上將會有多個線程並行(parallel)執行;擔當線程數多於處理器數時,依然會有多條線程在同一個CPU上輪換的現象。

  • 當一條線程開始運行後,它不可能一直處於運行狀態(除非線程執行體足夠短,瞬間就執行完了),線程在運行過程中需要被中斷,目的是使其他線程獲得執行的機會線程調度的細節取決於底層平臺所採用的策略

==搶佔式調度策略==(所有現代的桌面和服務器操作系統採用)

系統會給每個可執行的線程一小段的時間來處理任務;當該時間段使用完,系統就會剝奪該線程所佔據的資源,讓其他線程獲得執行的機會。在選擇下一個線程時,系統會考慮線程的優先級。

就緒和運行狀態之間的轉換通常不受程序控制,而是由系統線程調度所導致。


線程死亡

線程會以以下三種方式之一結束,結束後處於死亡狀態:

  • run方法執行完成,線程正常結束
  • 線程拋出一個未捕獲的Exception或Error
  • 直接調用該線程的stop方法來結束該線程(容易導致死鎖,不推薦使用!)

注意:
當主線程結束的時候,其他線程不受任何影響,並不會隨之結束。一旦子線程啓動起來後,它就擁有和主線程相同的地位,不會受主線程的影響(如前面所說,線程之間是相互獨立的)

爲了測試某條線程是否已經死亡,可以調用線程對象的isAlive方法。

  • 當線程處於就緒、運行、阻塞三種狀態時,該方法返回true
  • 處於新建、死亡兩種狀態時,返回false

不可以對一個已經死亡的線程調用start方法,否則會引發IllegalThreadStateException不能對已經死亡或者已經啓動的線程調用start方法,只能對新建狀態的線程調用)



3.控制線程

3.1 join

當在某個程序執行流中A調用其他線程的join方法,A(調用join方法的那個線程)將被阻塞,知道join方法加入的join線程完成爲止。

join方法通常由使用線程的程序調用,以將大問題劃分成許多小問題,每個小問題配一個線程,當所有的小問題得到處理後,再調用主線程來進一步操作。

例:

package thread.controlThread;

public class Join {

    public static void main(String[] args) {

        JoinThread jt = new JoinThread();

        new Thread(jt, "HelloWorld").start();

        for (int i = 0; i < 100; i++) {
            if (i == 20) {
                Thread joinThread = new Thread(jt, "joinThread");
                joinThread.start();
                try {
                    joinThread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            System.out.println(Thread.currentThread().getName() + " : " + i );
        }
    }

}


class JoinThread implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
        }
    }   
}

上面程序一共有3條線程:

  • 主線程開始之後啓動了名爲“HelloWorld”的線程,該子線程將會和main線程併發執行

  • 當主線程的循環變量i等於20時,啓動了名爲“joinThread”的線程,然後這個線程join進了main線程。注意:==此時“joinThread”不會和main線程併發執行,而是main線程必須等該線程執行結束後纔可以向下執行==。在“joinThread”執行時,實際上只有兩條子線程(“HelloWorld” 和 “joinThread”)併發執行,而main線程處於等待(阻塞)狀態知道“joinThread”執行完。


3.2 後臺程序(Daemon Thread)

  • 在後臺運行的 線程,任務是爲其他的線程提供服務。 JVM的垃圾回收線程就是典型的後臺線程。

  • ==特徵:如果所有的前臺線程都死亡,那麼後臺線程會自動死亡。==當整個虛擬機中只剩下後臺線程時,程序就沒有繼續運行的必要了,所以虛擬機也就退出了。

  • 設置指定線程爲後臺線程: ==調用Thread對象的setDaemon()方法==

  • 前臺線程創建的子線程默認是前臺線程,後臺線程創建的子線程默認是後臺線程

例:

package thread.controlThread;

public class DaemonThread {

    public static void main(String[] args) {
       Daemon d = new Daemon();
       Thread t = new Thread(d, "DaemonThread");

       t.setDaemon(true);

       t.start();

       for (int i = 0; i < 10; i++) {
           System.out.println(Thread.currentThread().getName() + " : " + i);
       }
    }

}

class Daemon implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
        }
    }

}

上面代碼在main方法裏先將t設置爲後臺線程,然後啓動該線程(==要將某個線程設置爲後臺線程,必須在該線程啓動之前設置,setDaemon(true)必須在start()之前調用,否則會引發IllegalThreadStateException==)。本來該線程和ing該執行到i = 99纔會結束,但是實際上它無法運行到99,因爲主線程(程序中唯一的前臺線程)運行結束後,JVM會自動退出,後臺線程也就自動死亡了。


3.3 線程睡眠:sleep

sleep方法:Thread類的靜態方法,讓當前正在執行的線程暫停一段時間,並且進入阻塞狀態。噹噹前線程調用sleep方法進入阻塞狀態之後,在它sleep的時間裏,它不會獲得執行的機會。就算系統中沒有其他可運行的程序,處於sleep的線程也不會運行,因此sleep方法常用於暫停程序的運行。

sleep有兩種重載方法:

  • static void sleep(long millis)

    讓當前正在執行的線程暫停millis個毫秒,並進入阻塞狀態,該方法受到系統計時器和線程調度器精度和準確度的影響。

  • static void sleep(long millis, int nanos)

    和前面一樣,很少使用。


3.4 線程讓步:yield

和sleep有點類似,也是Thread類的一個靜態方法。它也可以讓當前正在執行的線程暫停,但不會使線程阻塞,只是將線程轉入就緒狀態。

——> yield只是讓當前線程暫停一下下,讓系統的線程調度器重新調度一次。完全可能出現的情況是:當某個線程調用了yield方法暫停之後,線程調度器又將它調度出來執行。

實際上,當某個線程調用了yield之後,只有優先級與當前線程==相同==或者==比當前線程高==的==就緒狀態==的線程纔會獲得執行的機會。也就是說,如果當前線程優先級設成了Thread.MAX_PRIORITY的話,它yield之後其他線程不會獲得執行機會(除非其他線程中也有MAX——PRIORITY的線程),線程調度器又把它調出來運行。

yield的意思是屈服, 顧名思義,只有面對和它優先級相同或者比它更高的線程才能屈服,暫停自己讓別人運行,不可能屈服於比自己優先級低的線程。

sleep和yield的區別

sleep yield
sleep暫停當前線程之後,會給其他線程執行機會,並不考慮線程優先級 yield方法暫停當前線程之後,==只有和當前線程優先級相同或者更高的處於就緒狀態(runnable)的線程纔能有執行的機會==
sleep方法會將線程轉入阻塞狀態,知道經過了設定的阻塞時間纔會轉到就緒狀態 yield方法不會將線程轉入阻塞狀態,它只是強制讓當前線程從運行狀態(runnig)轉到就緒狀態(runnable)。因此完全有可能某個線程調用yield暫停之後又馬上獲得CPU資源被執行
sleep方法會拋出InterruptedException異常 不拋異常
sleep方法比yiled方法具有更好的移植性 通常不要依靠yield來控制併發線程的執行


3.5 改變線程的優先級

  • 每個線程執行時都有一定的優先級,優先級高的線程獲得較多的執行機會,優先級低的獲得較少的執行機會。

  • 每個線程默認的優先級都與創建它的父線程具有相同的優先級,默認情況下,main線程的具有普通的優先級,它創建的子線程也具有普通的優先級

  • 通過`setPriority(int newPriority)來設置指定線程的優先級,參數可以是一個1 ~ 10之間的整數,也可以使用Thread類的三個靜態常量:

    • MAX_PRIORITY : 10
    • MIN_PRIORITY : 1
    • NORM_PRIORITY : 5

    雖然java提供了10個優先級別,但是這些優先級需要操作系統的支持。不同的操作系統上優先級並不相同,而且也不能很好的和java中的10個優先級對應。——> ==所以應該金蓮高避免直接爲線程指定優先級,而是使用三個經常常量來設置優先級,這樣纔可以保證程序具有最好的可移植性。==

  • 通過getPriority()來返回當前線程的優先級




4.線程的同步

多線程編程容易突然出現錯誤,這是因爲系統的線程調度具有一定的隨機性,也說明了編程不當。在使用多線程的時候,必須要保證線程安全。

4.1 線程安全

考慮一個經典的問題:銀行取錢,基本流程分爲如下幾個步驟:

  1. 用戶輸入賬戶、密碼,系統判斷用戶的賬戶和密碼是否匹配

  2. 用戶輸入取款金額

  3. 系統判斷用戶餘額是否大於取款金額

  4. 餘額 > 取款金額,取款成功,用戶取出鈔票;否則取款失敗。

現在模擬最後三步操作,使用兩條線程來模擬兩個人分別在不同的取款機上對同一個賬戶併發進行取款。

代碼如下:

  • Account
package thread.synchronize;

public class Account {

    private String accountNr;
    private double balance;

    public Account() {

    }

    public Account(String accountNr, double balance) {
        this.accountNr = accountNr;
        this.balance = balance;
    }

    public String getAccountNr() {
        return accountNr;
    }

    public void setAccountNr(String accountNr) {
        this.accountNr = accountNr;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    public int hashCode() {
        return this.accountNr.hashCode();
    }

    public boolean equals(Account account) {
        return account.getAccountNr().equals(this.accountNr);
    }
}
  • DrawThread:

package thread.synchronize;

public class DrawThread extends Thread {

    private Account account;
    private double drawAmount;

    public DrawThread(String name, Account account, double amount) {
        super(name);
        this.account = account;
        this.drawAmount = amount;
    }


    public void run() {

        if (this.account.getBalance() >= this.drawAmount) {
            System.out.println(this.getName() + " : Success! draw : " + this.drawAmount);

            try {
                this.sleep(1);
            } catch(InterruptedException e) {
                e.printStackTrace();
            }

            this.account.setBalance(this.account.getBalance() - this.drawAmount);
            System.out.println("balance : " + this.account.getBalance());
        } else {
            System.out.println(this.getName() + " : Fail...Not enough balance");
        }
    }

}
  • TestDraw:
package thread.synchronize;

public class TestDraw {

    public static void main(String[] args) {
        Account account = new Account("1568856", 1000);
        new DrawThread("A", account, 800).start();;
        new DrawThread("B", account, 800).start();;

    }

}

結果:

B : Success! draw : 800.0
A : Success! draw : 800.0
balance : 200.0
balance : 200.0

這個結果明顯是不符合預期的,取了兩次800,賬戶卻只扣除了一次。爲什麼會出現這種情況呢?這是因爲run方法的方法體不具有同步安全性:程序中由兩條併發的線程修改Account對象,而且系統恰好在run方法裏

    this.sleep(1)

這行代碼處進行線程切換:

Takt A B
1 balance = 1000,取出錢 nop
2 sleep(1) balance = 1000,取出錢
3 balance = 1000 - 800 = 200 sleep(1)
4 nop balance = 1000 - 800 = 200

所以最後A和B各取了1000,賬戶只扣了800.

爲了解決這個問題,java的多線程引入了==同步監視器==來解決這個問題。

使用同步監視器的通用方法就是==同步代碼塊==,語法如下:

synchronized(obj) {

    //bla bla bla

}

synchronized後括號裏的obj就是同步鎖定器。上面代碼的含義是:==線程開始執行同步代碼塊之前,必須先獲得對同步鎖定器的鎖定。==

使用同步監視器的目的是:防止兩條線程對同一個共享資源進行併發訪問,因此==通常用可能被併發訪問的共享資源當同步監視器==(比如上面的Account對象)。

使用同步監視器的邏輯是:

==加鎖 ——> 修改 ——> 修改完成 ——> 釋放鎖==

任何線程想修改指定資源之前,首先對該資源加鎖,在加鎖期間其他線程無法修改該資源,修改完成後,該線程釋放對資源的鎖定,然後其他線程才能訪問或修改這個資源。通過這種方式就可以==保證在任一時刻只有一條線程可以進入修改共享資源的代碼區(==同步代碼塊,也被稱作臨界區),所以同一時刻僅僅有一條線程處於臨界區內,從而保證了線程的安全性。

依照這個思路,修改上面的代碼:

package thread.synchronize;

public class DrawThread extends Thread {

    private Account account;
    private double drawAmount;

    public DrawThread(String name, Account account, double amount) {
        super(name);
        this.account = account;
        this.drawAmount = amount;
    }

    public void run() {

        synchronized (this.account) {

            if (this.account.getBalance() >= this.drawAmount) {
                System.out.println(this.getName() + " : Success! draw : "
                        + this.drawAmount);

                try {
                    this.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                this.account.setBalance(this.account.getBalance()
                        - this.drawAmount);
                System.out.println("balance : " + this.account.getBalance());
            } else {
                System.out.println(this.getName()
                        + " : Fail...Not enough balance");
            }
        }
    }

}



4.2 同步方法

  • 同步方法就是使用synchronized關鍵字來修飾某個方法,該方法稱爲同步方法

  • 對於同步方法而言,無需顯式指定同步監視器==同步方法的同步監視器就是this,也就是對象本身==

  • synchronized關鍵字可以修飾方法,可以修飾代碼塊,但是不能修飾構造器,屬性等。

例:將上面的例子使用同步方法來保證線程安全:

修改Account,增加一個用synchronized關鍵字修飾的draw方法

package thread.synchronize;

public class Account {

    private String accountNr;
    private double balance;

    public Account() {

    }

    public Account(String accountNr, double balance) {
        this.accountNr = accountNr;
        this.balance = balance;
    }

    public String getAccountNr() {
        return accountNr;
    }

    public double getBalance() {
        return balance;
    }

    public synchronized void draw(double drawAmount) {
        if (this.balance >= drawAmount) {
            System.out.println(Thread.currentThread().getName() + " : Success! draw : " + drawAmount);
            try {
                Thread.sleep(1);
            } catch(InterruptedException e) {
                e.printStackTrace();
            }

            this.balance -= drawAmount;
        } else {
            System.out.println(Thread.currentThread().getName()
                    + " : Fail...Not enough balance");
        }
    }

    public int hashCode() {
        return this.accountNr.hashCode();
    }

    public boolean equals(Account account) {
        return account.getAccountNr().equals(this.accountNr);
    }
}

上面的代碼還刪去了balance屬性的setter方法,因爲賬戶餘額不能隨便修改。

同步方法的同步監視器是this,==因此對於同一個Account對象而言,任意時刻只能有一條線程獲得對Account的鎖定,然後進入draw方法執行取錢操作==,這樣也可以保證多條線程併發取錢的線程安全。

因爲Account類已經提供了draw方法,而且取消了setBalance()方法,所以還得修改DrawThread類:該類只需直接調用Account對象的draw方法來執行取錢操作。

package thread.synchronize;

public class DrawThread extends Thread {

    private Account account;
    private double drawAmount;

    public DrawThread(String name, Account account, double amount) {
        super(name);
        this.account = account;
        this.drawAmount = amount;
    }

    public void run() {
        this.account.draw(this.drawAmount);
    }

}

因爲已經在Account類中使用synchronized保證了draw方法的線程安全性,所以在run方法裏直接調用也沒有任何問題。

這時的程序把draw方法定義在Account裏,而不是在run方法裏實現取錢邏輯,這種做法更符合面向對象。在面向對象裏有一種流行的設計方式:==Domain Drive Design(領域驅動設計,DDD),這種方式認爲每個類都應該是完備的領域對象。==例如Account代表賬戶,應該提供賬戶的相關方法,例如通過draw方法來執行取錢操作,而不是直接將setBalance方法暴露出來任人操作,這樣纔可以更好的保證Account對象的完整性、一致性

可變類的線程安全是以降低程序的運行效率作爲代價的,爲了減少保證線程安全而帶來的負面影響,可以採取以下策略:

  • 不要對線程安全類的所有方法都進行同步(都用synchronized修飾),只對那些會改變競爭資源(共享資源)的方法進行同步。例如上面Account類的hashCode和equals等方法無需同步

  • 如果可變類有兩種運行環境:單線程和多線程環境,則應該爲該類提供兩種版本:線程不安全版本(用於在單線程環境中以保證性能),線程安全版本(在多線程環境中保證線程安全)


4.3 釋放同步監視器的鎖定

任何線程進入同步代碼塊,同步方法之前,必須先獲得對同步監視器的鎖定,那麼何時會釋放對同步監視器的鎖定呢?

程序無法顯式的釋放對同步監視器的鎖定,線程會在如下幾種情況釋放:

  • 當前線程的同步方法、同步代碼塊執行結束

  • 當前線程的同步方法、同步代碼塊遇到**break、retur**n終止了該方法的繼續執行

  • 當前線程的同步方法、同步代碼塊出現了未處理的Error或Exception,導致了該方法、代碼塊異常結束

  • ==當前線程的同步方法、同步代碼塊執行了同步監視器對象的wait方法==

在下面情況下,線程不會釋放同步監視器:

  • 當前線程執行同步方法、同步代碼塊時,程序調用Thread.sleep(),Thread.yield()方法來暫停當前線程的執行

  • 當前線程執行同步代碼塊、同步方法時,其他線程調用了當前線程的suspend方法將它掛起(當然我們應該儘量避免使用suspend和resume方法)


4.4同步鎖(Lock)

另一種線程同步的機制:它通過顯式定義同步鎖對象來實現同步,在這種機制下,同步鎖應該使用Lock對象更適合。

Lock提供了比synchronized方法和代碼塊更廣泛的鎖定操作,Lock對象實現允許更靈活的結構,可以具有差別很大的屬性,並可以支持多個相關的Condition對象。

Lock是控制多個線程對共享資源進行訪問的工具。通常,鎖提供了對共享資源的獨佔訪問,每次==只能有一個線程==對Lock對象加鎖,線程開始訪問共享資源之前應先獲得Lock對象。不過,某些鎖可能允許對共享資源進行併發訪問,比如ReadWriteLock

在實現線程安全的控制中,通常用ReentrantLock,使用該Lock對象可以顯式的加鎖、釋放鎖。

語法如下

class X {

    // 定義鎖對象
    private final ReentrantLock lock = new ReentranLock();

    // 定義需要保證線程安全的方法
    public void m() {

        // 加鎖
        lock.lock();

        try {
            //需要保證線程安全的代碼
            // body
        } finally {

            // 使用finally來保證釋放鎖
            lock.unlock(); 

        }

    }
}

例:根據Lock來修改上面的Account類:

package thread.synchronize;

import java.util.concurrent.locks.ReentrantLock;

public class Account {

    private ReentrantLock lock = new ReentrantLock();

    private String accountNr;
    private double balance;

    public Account() {

    }

    public Account(String accountNr, double balance) {
        this.accountNr = accountNr;
        this.balance = balance;
    }

    public String getAccountNr() {
        return accountNr;
    }

    public double getBalance() {
        return balance;
    }

    public void draw(double drawAmount) {

        lock.lock();
        try {
            if (this.balance >= drawAmount) {
                System.out.println(Thread.currentThread().getName()
                        + " : Success! draw : " + drawAmount);
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                this.balance -= drawAmount;
            } else {
                System.out.println(Thread.currentThread().getName()
                        + " : Fail...Not enough balance");
            }
        } finally {
            lock.unlock();
        }
    }

    public int hashCode() {
        return this.accountNr.hashCode();
    }

    public boolean equals(Account account) {
        return account.getAccountNr().equals(this.accountNr);
    }
}
  • 使用Lock與使用同步方法有點相似,只是使用Lock時==顯式==使用Lock對象作爲同步鎖,而使用同步方式時系統==隱式==的使用當前對象作爲同步監視器,同樣都符合“加鎖 –> 訪問 –> 釋放鎖”的模式;而且==使用Lock對象時每個Lock對象對應一個Account對象==,同樣能保證對於同一個Account對象,同一時刻只有一條線程進入臨界區。

  • Lock提供了同步方法和同步代碼塊沒有的其他功能,包括用於非塊結構的tryLock方法、試圖獲取可中斷鎖lockInterruptibly方法、獲取超時失效鎖的tryLock(long, TimeUnit)方法

  • ReentrantLock具有==重入性==,也就是說線程可以對它已經加鎖的ReentrantLock鎖再次加鎖,ReentrantLock對象會維持一個==計數器==來追蹤lock方法的嵌套調用==線程在每次調用lock方法加鎖後,必須顯式的使用unlock來解鎖。==

4.5 死鎖(DeadLock)

==當兩個線程相互等待對方釋放同步監視器時就會發生死鎖==,java虛擬機沒有檢測,也沒有采用措施來處理死鎖情況,一旦出現死鎖,整個程序既不會發生任何異常,也不會給出任何提示,只是所有線程處於阻塞狀態,無法繼續運行。

死鎖是很容易發生的,尤其是出現多個同步監視器的時候。

例:

  • A類:
package thread.deadLock;

public class A {

    public synchronized void foo(B b) {
        System.out.println(Thread.currentThread().getName() + " :entering method foo of A...");

        try {
            Thread.sleep(200);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + " :try to enter method last of B...");
        b.last();

    }

    public synchronized void last() {
        System.out.println("Got into method last of A");
    }
}
  • B類:
package thread.deadLock;

public class B {

    public synchronized void bar(A a) {
        System.out.println(Thread.currentThread().getName() + " :entering method bar of B...");

        try {
            Thread.sleep(200);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + " :try to enter method last of A...");
        a.last();
    }

    public synchronized void last() {
        System.out.println("got into methood last of B");
    }

}
  • DeadLock:
package thread.deadLock;

public class DeadLock implements Runnable {

    A a = new A();
    B b = new B();

    public void init() {
        Thread.currentThread().setName("main Thread");
        a.foo(b);
        System.out.println("After starting main Thread...");
    }


    @Override
    public void run() {
        Thread.currentThread().setName("vice Thread");
        b.bar(a);
        System.out.println("After entering vice Thread...");
    }



    public static void main(String[] args) {
        DeadLock dl = new DeadLock();

        new Thread(dl).start();
        dl.init();
    }

}

運行結果:

main Thread :entering method foo of A...
vice Thread :entering method bar of B...
main Thread :try to enter method last of B...
vice Thread :try to enter method last of A...

從結果中可以看出:程序既無法向下執行,也不會拋出任何異常,只是都卡在兩個對象的last方法那裏,僵持着無法向下執行。

這是因爲:

  • 上面程序中A對象和B對象的方法都是同步方法,即:==A對象和B對象都是同步鎖==

  • 程序中有兩條線程在執行,一條線程的執行體是DeadLock類是run方法(副線程),另一條線程的執行體是DeadLock的init方法(主線程調用了init方法)。run方法讓B對象調用bar方法,而init方法讓A對象調用foo方法。

  • init方法首先執行,調用了A對象的foo方法,因爲foo方法是一個同步方法,所以進入foo方法之前,主線程會首先對A對象加鎖。執行到Thread.sleep(200);時,主線程休眠200個毫秒

  • 然後副線程開始執行,調用B對象的bar方法,同樣的,由於bar方法也是一個同步方法,所以進入bar方法之前,副線程會首先對B對象加鎖。執行到Thread.sleep(200);時,副線程休眠200個毫秒

  • 接下來主線程繼續向下執行直到b.last();,此處希望調用b的last方法;由於last方法也是一個同步方法,所以主線程在執行之前必須先對B對象加鎖,==但此時副線程正保持着B對象的鎖,主線程無法對B對象加鎖==。所以b.last();無法執行,主線程進入阻塞狀態。

  • 接着副線程醒過來繼續向下執行直到a.last();,此處希望調用a的last方法,由於last方法也是一個同步方法,所以在副線程執行之前必須先對A對象加鎖,==但此時主線程正保持着A對象的鎖,副線程無法對A對象加鎖==。所以a.last();無法執行,副線程也進入阻塞狀態。

  • 此時的狀況就是:==主線程保持着A對象的同步鎖,等待B對象解鎖之後執行b.last();;副線程保持着B對象的同步鎖,等待A對象解鎖之後執行a.last(),兩條線程互相等待對方先釋放鎖,誰也不讓誰,所以就出現了死鎖!==




5.線程通信

5.1 線程的協調運行

假設現在系統中由兩條線程,分別代表存款者、取款者——現在有一個需求:要求存款者和取款者不斷重複存款、取款兩個操作,並且要求每當存款者將錢存入賬戶後,取款者馬上從賬戶中取走這筆錢。不允許存款者、取款者連續兩次存款、取款。

爲了實現這個功能,可以使用Object類提供的wait(),notify(),notifyAll()三個方法。注意:這三個方法並不屬於Thread類,而是屬於Object類。但這三個方法必須由同步監視器對象來調用,可分成兩種情況:

  • 對於使用synchronized來修飾的同步方法,對象本身就是同步監視器,所以可以在同步方法中直接使用這三個方法。

  • 對於使用synchronized修飾的同步代碼塊,同步監視器是synchronized後括號裏的對象,所以必須使用該對象來調用這三個方法。

三個方法:

  • wait()

    導致當前線程等待,知道其他線程調用該同步監視器的notify()或者notifyAll()來喚醒該線程。三種重載:

    • 無參數的wait,一直等待,知道其他線程通知喚醒
    • 帶毫秒參數的wait
    • 帶毫秒、微秒參數的wait

    後面兩種方法都是等待制定時間後==自動甦醒==過來。

    ==調用wait方法的線程會釋放對該同步監視器的鎖定!==

  • notify()

    喚醒在此同步監視器等待的單個線程。如果所有線程都在此同步監視器上等待,那麼會隨機喚醒一條線程。但並不是說喚醒之後就執行被喚醒的線程,而是當前線程放棄對該同步監視器的鎖定後(使用wait方法),纔可以執行被喚醒的線程。(因爲任意時刻只能有一條線程鎖定同步監視器)

  • notify()All

    喚醒在此同步監視器上等待的所有線程。同樣的,只有當前線程放棄對該同步監視器的鎖定後(使用wait方法),纔可以執行被喚醒的線程。

思路:在Account類中可以設一個Tag isDeposited來標記賬戶中是否已經有存款:

  • 當tag是false,表明賬戶中沒有存款,存款者可以繼續向下執行;存款者把錢存入賬戶後,tag設爲true(已經存入款項),並調用notifyAll來喚醒其他線程;當存款者線程進入線程體後,如果tag是true(已經有存款,不能重複存),那麼就調用wait方法來阻塞它。

  • 當tag是true,表明賬戶中已經存入了錢,則取款者可以繼續向下執行,當取款者把錢從賬戶中取出後,將tag設爲false(錢已取出,賬戶中沒有存款了),tag設爲true,並調用notifyAll來喚醒其他線程;當存款者線程進入線程體後,如果tag是false(賬戶中沒有存款,自然就不能取款了),那麼就調用wait方法來阻塞它。

代碼如下:

  • Account:
package thread.threadCommunitcation;

import java.util.concurrent.locks.ReentrantLock;

public class Account {

    private ReentrantLock lock = new ReentrantLock();

    private String accountNr;
    private double balance;

    // use isDeposited to indicate whether the account is deposited
    private boolean isDeposited;  


    public Account() {

    }

    public Account(String accountNr, double balance) {
        this.accountNr = accountNr;
        this.balance = balance;
    }

    public String getAccountNr() {
        return accountNr;
    }

    public double getBalance() {
        return balance;
    }

    public synchronized void draw(double drawAmount) {
        try {
            if (!isDeposited) {
                this.wait();
            } else {
                System.out.println(Thread.currentThread().getName()
                        + " draw : " + drawAmount);
                this.balance -= drawAmount;
                System.out.println("Now the balance is: " + this.balance + "\n");

                // After drawing set isDeposited to false
                this.isDeposited = false;
                this.notifyAll();
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public synchronized void deposit(double depoAmount) {
        try {
            if (isDeposited) {
                this.wait();
            } else {
                System.out.println(Thread.currentThread().getName() + " deposit : " + depoAmount);
                this.balance += depoAmount;
                System.out.println("Now the balance is: " + this.balance + "\n");

                // After depositing set isDeposited to true
                this.isDeposited = true;
                notifyAll();
            }
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
    }

    public int hashCode() {
        return this.accountNr.hashCode();
    }

    public boolean equals(Account account) {
        return account.getAccountNr().equals(this.accountNr);
    }
}

因爲draw和deposit兩個方法會併發修改Account的balance屬性,所以要設成==synchronized==來保證線程安全。

  • 取款者線程:
package thread.threadCommunitcation;

public class DrawThread extends Thread {

    private Account account;
    private double drawAmount;

    public DrawThread(String name, Account account, double amount) {
        super(name);
        this.account = account;
        this.drawAmount = amount;
    }

    public void run() {

        for(int i = 0; i < 20; i++) {
            this.account.draw(this.drawAmount);            
        }
    }

}
  • 存款者線程:
package thread.threadCommunitcation;

public class DepositThread extends Thread{

    private Account account;
    private double depoAmount;

    public DepositThread(String name, Account account, double depoAmount) {
        super(name);
        this.account = account;
        this.depoAmount = depoAmount;
    }

    public void run() {

        for (int i = 0; i < 20; i++) {
            this.account.deposit(this.depoAmount);
        }
    }

}

對存款者線程而言,進入Account類的deposit方法後,如果isDeposited == true,表明賬戶中已經有存款,不能重複存款,所以調用wait方法阻塞;否則(isDeposited == flase)程序向下執行存款操作,存款操作完成後,將isDeposited設成true,然後調用notifyAll來喚醒其他被阻塞的線程——如果系統中有其他存款者線程,也會被喚醒,但被喚醒的存款者線程執行到if (isDeposited)這一句時,因爲之前的存款者線程已經將isDeposited設成了true,即此時Account中已有存款,所以該存款者線程會再次進入阻塞(調用wait);只有執行draw方法的取款者線程纔可以繼續向下執行。同理,取款者線程的流程也是如此。

  • 測試類:

假設有三個存款者和一個取款者

package thread.threadCommunitcation;

public class Demo {

    public static void main(String[] args) {

        Account account = new Account("!568856", 0);

        new DrawThread("draw1", account,800).start();

        new DepositThread("depo1", account, 800).start();;
        new DepositThread("depo2", account, 800).start();;
        new DepositThread("depo3", account, 800).start();;
    }

}

運行程序可以看到:存款者線程、取款者線程交替存、取錢(三個存款者線程隨機向賬戶中存錢)。

執行到最後會看到這樣的結果:

draw1 draw : 800.0
Now the balance is: 0.0

depo1 deposit : 800.0
Now the balance is: 800.0

draw1 draw : 800.0
Now the balance is: 0.0

depo2 deposit : 800.0
Now the balance is: 800.0

程序最後阻塞無法繼續向下運行,這是因爲:3個存款者線程,一共會存款 3 * 20 = 60 次,而只有一個取款者線程,總共只取20次,所以當取款者線程執行完最後一次取款操作後,調用notifyAll來喚醒三個存款者線程,然後其中一個存款者線程往賬戶裏存錢之後,調用notifyAll來喚醒其他線程,==但此時能喚醒的只有另外兩個存款者線程(因爲取款者線程已經執行完最後一次取款操作,以後不會再執行了)==,另外兩個存款者線程進入deposit方法後,isDeposit都是true,所以都會調用wait方法,然後大家都wait在那裏了,所以程序也就阻塞了,不能繼續向下運行了。

注意這裏的阻塞並不是死鎖!在這裏取款者線程已經執行結束,==而存款者線程只是在那裏等待其他線程來取錢而已,而不是等待其他線程釋放同步監視器==。切勿把死鎖和阻塞混淆起來!



5.2 使用條件變量控制線程協調

  • 如果程序不使用synchronized關鍵字來保證同步,而是直接使用Lock對象來保證同步,則系統中不存在隱式的同步監視器對象,也就不能使用wait、notify、notifyAll來協調進程的進行。

  • 當使用Lock對象來保證同步時,java提供了一個Condition類來保持協調,使用Condition可以讓那些已經得到Lock對象卻無法繼續執行的線程釋放Lock對象,也可以喚醒其他處於等待的線程

  • Condition將同步監視器方法wait、notify、notifyAll分解成截然不同的對象,以便通過將這些對象與Lock對象組合使用,爲每個對象提供多個等待集(Wait-set)。

    ——> Lock替代了同步方法、同步代碼塊,Condition替代了同步監視器。

  • Conditon實例被綁定在一個Lock對象,調用Lock對象的newCondintion()方法可以獲得Condition的實例。

  • Condition類提供了三個方法:

    • await()

      類似於隱式同步監視器的wait方法,導致當前線程等待,直到其他線程調用signal或者signalAll方法來喚醒該線程。

      該方法有更多的變體,具體查API

    • signal()

      類似於notify方法,喚醒在此Lock對象上等待的多個線程。如果所有線程都在該Lock對象上等待,則會隨機選擇喚醒一個線程。只有當前線程放棄對該Lock對象的鎖定後(使用await方法),纔可以執行被喚醒的線程。

      (與notify極其類似!!!)

    • signAll()

      喚醒在此Lock對象上等待的所有線程。同樣的,只有當前線程放棄對該Lock對象的鎖定後(使用await方法),纔可以執行被喚醒的線程。

使用Lock和Condition來修改Account類:


package thread.threadCommunitcation;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class Account {

    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = this.lock.newCondition();

    private String accountNr;
    private double balance;

    // use isDeposited to indicate whether the account is deposited
    private boolean isDeposited;  


    public Account() {

    }

    public Account(String accountNr, double balance) {
        this.accountNr = accountNr;
        this.balance = balance;
    }

    public String getAccountNr() {
        return accountNr;
    }

    public double getBalance() {
        return balance;
    }

    public void draw(double drawAmount) {

        this.lock.lock();

        try {
            if (!isDeposited) {
                this.condition.await();
            } else {
                System.out.println(Thread.currentThread().getName()
                        + " draw : " + drawAmount);
                this.balance -= drawAmount;
                System.out.println("Now the balance is: " + this.balance + "\n");

                // After drawing set isDeposited to false
                this.isDeposited = false;
                this.condition.signalAll();
            }

        } catch (InterruptedException e) {
            e.printStackTrace();

        } finally {
            this.lock.unlock();
        }
    }

    public void deposit(double depoAmount) {

        this.lock.lock();

        try {
            if (isDeposited) {
                this.condition.await();
            } else {
                System.out.println(Thread.currentThread().getName() + " deposit : " + depoAmount);
                this.balance += depoAmount;
                System.out.println("Now the balance is: " + this.balance + "\n");

                // After depositing set isDeposited to true
                this.isDeposited = true;
                this.condition.signalAll();
            }
        } catch(InterruptedException e) {
            e.printStackTrace();

        } finally {
            this.lock.unlock();
        }
    } 

    public int hashCode() {
        return this.accountNr.hashCode();
    }

    public boolean equals(Account account) {
        return account.getAccountNr().equals(this.accountNr);
    }
}
  • Account類增加了屬性condition
  • deposit和draw兩個方法不需要再用synchronized修飾
  • 在兩個方法的方法體裏顯式的加鎖this.lock.lock();和解鎖this.lock.unlock();


5.3使用管道流

前面介紹的兩種方式可以稱之爲線程之間協調運行的控制策略。如果需要在兩條線程之間進行更多的信息交互,可以考慮使用管道流進行通信。

管道流油三種形式:

  • 管道字節流

    • PipedInputStream
    • PipedOutputStream
  • 管道字符流

    • PipedReader
    • PepedWriter
  • 新IO的管道Channel

    • Pipe.SinkChannel
    • Pipe.SourceChannel


==使用管道流實現多線程通信的步驟:==

  1. 使用new字符分別創建管道輸入流和管道輸出流

  2. 使用管道輸入流或管道輸出流的connect方法把兩個輸入流和輸出流連接起來

  3. 將管道輸入流、管道輸出流分別傳入兩個線程

  4. 兩個線程分別依賴各自的管道輸入流、管道輸出流進行通信

例子:

  • ReadThread
package thread.readAndWrite;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.PipedReader;

public class ReadThread extends Thread {

    private PipedReader pr;
    private BufferedReader br;

    public ReadThread(){

    }

    public ReadThread(PipedReader pr) {
        this.pr = pr;
        this.br = new BufferedReader(pr); // decorator
    }

    public void run() {
        String read = null;

        try {

            while((read = this.br.readLine()) != null) {
                System.out.println(read);
            }
        } catch(IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (this.br != null);
                br.close();
            } catch(IOException e) {
                e.printStackTrace();
            }
        }
    }

}
  • WriteThread
package thread.readAndWrite;

import java.io.IOException;
import java.io.PipedWriter;

public class WriteThread extends Thread {

    private String[] strArr = new String[] { "A", "B", "C", "D" };

    private PipedWriter pw;

    public WriteThread() {

    }

    public WriteThread(PipedWriter pw) {
        this.pw = pw;
    }

    public void run() {

        try {
            for (int i = 0; i < 10; i++) {
                pw.write(strArr[i % 4] + "\n");
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (this.pw != null) {
                    pw.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}
  • 測試類
package thread.readAndWrite;

import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;

public class TestCommunication {

    public static void main(String[] args) {


        PipedWriter pw = null;
        PipedReader pr = null;

        try {

            // 1. Create PipedWriter and PipedReader
            pw = new PipedWriter();
            pr = new PipedReader();

            // 2. connect
            pw.connect(pr);

            // 3. Now is ready for communication between these two threads
            new WriteThread(pw).start();
            new ReadThread(pr).start();
        } catch(IOException e) {
            e.printStackTrace();
        }

    }

}

不難發現,其實跟IO並沒有什麼區別,關鍵在於通過pw.connect(pr);這一句把PipedWriter和PipeReader連接起來實現線程通信。

通常沒有必要使用管道流來控制兩個線程之間的通信,因爲兩個線程屬於同一個進程,它們可以肥城方便的共享數據,這種方式纔是線程之間進行信息交換的最好方式,而不是使用管道流。

—–> 現在纔來告訴我然而並沒有什麼卵用。。。。



5.4 線程組(ThreadGroup)

  • java使用ThreadGroup來表示線程組,它可以對一批線程進行分類管理,對線程組的控制相當於同時控制這一批線程。

  • 用戶創建的所有線程都屬於==指定線程組==。如果程序沒有顯式的制定線程屬於哪個線程組,那麼該線程屬於默認線程組。默認情況下,子線程和創建它的父線程處於同一個線程組內:例如A創建了B並且沒有指定B的線程組,那麼B屬於線程A所在的線程組

  • 一旦某個線程加入了制定線程組之後,該線程將一直處於該線程組,知道該線程死亡,線程運行中途==不能改變它所屬的線程組==。因此Thread類並沒與提供setThreadGroup這樣一個方法來改變線程所屬的線程組,但是提供了一個getThreadGroup方法來返回該線程所屬的線程組。

  • Thread類提供下面幾個構造器來設置新家的線程屬於哪個線程組:

    • Thread(ThreadGroup group, Runnable target)

    • Thread(ThreadGroup group, Runnnale target, String name)

    • Thread(ThreadGroup group, String name)
  • ThreadGroup類的構造方法:

    • ThreadGroup(String name)

    • Thread(ThreadGroup parent, String name)

      以指定的名字和父線程組來創建一個新線程組

    上面兩個構造方法創建線程組實例時都必須爲其制定一個名字,也就是線程組總是有一個字符串名字,改名字可調用ThreadGroup的getName()得到,==但不允許改變線程組的名字!==

  • 常用方法:

    • int activeCount()

      返回此線程組中活動線程的數目

    • interrupt()

      中斷此線程組的所有線程

    • isDaemon()

      判斷該線程是否是後臺線程組

    • setDaemon(boolean daemon)

      把該線程組設廠後臺線程組。後天線程組的特徵是:當後條線程組的最後一個線程執行結束或最後一個線程被銷燬,那麼後臺線程組將自動銷燬

    • setMaxPriority(int pri)

      設置線程組的最高優先級

例:

package thread.threadGroup;

public class TestThread extends Thread {


    public TestThread(String name) {
        super(name);
    }

    public TestThread(ThreadGroup group, String name) {
        super(group, name);
    }

    public void run() {

        for (int i = 0; i < 10; i++) {
            System.out.println(this.getName() + " : i = " + i);
        }
    }

}
  • 測試類:
package thread.threadGroup;

public class ThreadGroupTest {

    public static void main(String[] args) {

        ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
        System.out.println("Main ThreadGroup is : " + mainGroup.getName());
        System.out.println("Is main ThreadGroup a daemon ThreadGroup ? " + mainGroup.isDaemon());
        new TestThread("Thread of main ThreadGroup").start();

        System.out.println();

        ThreadGroup viceGroup = new ThreadGroup("Vice Group");
        viceGroup.setDaemon(true);
        System.out.println("Is Vice Group a daemon threadGroup ? " + viceGroup.isDaemon());

        TestThread t1 = new TestThread(viceGroup, "Thread-1 from viceGroup");
        t1.start();

        new TestThread(viceGroup, "Thread-2 from viceGroup").start();
    }

}


5.5 線程異常處理

ThreadGroup中還定義了一個很有用的方法:void uncaughtException(Thread t, Throwable e),該方法可以哦你過來處理該線程組內的線程所拋出的沒有處理的異常。

  • 如果線程執行過程中拋出了一個未處理的異常,JVM會在結束該線程之前自動查找是否有對應的Thread.UncauchtExceptionHandler對象,如果找到該處理器對象,就會調用該對象的void uncaughtException(Thread t, Throwable e)來處理該異常

  • Thread.UncauchtExceptionHandler是Thread類的一個==內部公共靜態接口。==這個接口內只有一個方法:void uncaughtException(Thread t, Throwable e),t代表出現異常的線程,e代表該異常拋出的異常。

  • Thread類中提供了兩個方法來設置異常處理器:

    • static setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)

      爲該線程類的所有線程實例設置默認的異常處理器(它是static的)

    • setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)

      爲制定線程實例設置異常處理器

  • ThreadGroup類實現了Thread.UncauchtExceptionHandler接口,所以每個線程所屬的線程組將會作爲默認的異常處理器。當一個線程拋出未處理的異常時,JVM會首先查找該異常對應的異常處理器(即setUncaughtExceptionHandler設置的異常處理器)。

    • 如果找到該異常處理器,則調用該異常處理器來處理異常

    • 如果沒有找到,JVM將會帶哦用該線程所屬的線程組的對象的void uncaughtException(Thread t, Throwable e)來處理該異常。


    ==線程組處理異常的默認流程==

    1. 如果該線程組有父線程組,那麼調用父線程組的void uncaughtException(Thread t, Throwable e)來處理該異常

    2. 如果該線程實例所屬的線程類有默認的異常處理器(static setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)方法設置的異常處理器),那就調用該異常處理器來處理該異常

    3. 如果該異常對象是ThreadDeath的對象,不做任何處理;否則將StackTrace的信息打印到System.err錯誤輸出力,結束該線程



例:

下面自定義一個線程的異常處理器,然後和JVM默認的線程處理器對比一下

  • 自定義的線程異常處理器
package thread.exceptionHandler;

public class MyExHandler implements Thread.UncaughtExceptionHandler {

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println(e + " appears in " + t);
    }

}

測試類:

package thread.exceptionHandler;

public class TestExHandler {

    public static void main(String[] args) {

        int a = 5 / 0;
    }

}

毫無疑問main方法裏僅有的一句代碼會引發除0異常

輸出的異常信息:

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at thread.exceptionHandler.TestExHandler.main(TestExHandler.java:8)

然後我在int a = 5 / 0之前設定main線程的默認異常處理器爲我自定義的異常處理器:

package thread.exceptionHandler;

public class TestExHandler {

    public static void main(String[] args) {

        // set the new Handler
        Thread.setDefaultUncaughtExceptionHandler(new MyExHandler());

        int a = 5 / 0;
    }

}

這樣main線程的默認異常處理器就設成了我自定義的異常處理器

輸出的異常信息:

java.lang.ArithmeticException: / by zero appears in Thread[main,5,main]



5.6 Callabe 和 Future

Callable是Runnable接口的增強版,Callable也提供了一個call()方法作爲線程執行體,但它比run方法更加強大牛逼:

  • call()方法可以有返回值

  • call()方法可以聲明拋出異常

類似於Runnable的用法,我們可以提供一個Callable對象作爲Thread的target,而call()方法作爲該線程的線程執行體。問題是:Callable接口並不是Runnable接口的子接口,而Thread的構造方法裏形參的類型是Runnable,所以Callable對象不能直接作爲Thread的target;而且call方法還有一個返回值——但call方法不是直接調用,而是作爲線程執行體被調用的。爲了解決這幾個問題,java提供了Future接口來代表Callable接口裏call方法的返回值,並且爲Future接口提供了一個FutureTask實現類,這個類實現了Future接口,也實現了Runnable接口(Adapter模式),這樣就可以作爲Thread類的target了。

在Future接口裏定義了下面幾個公共方法來控制它關聯的Callable任務:

  • boolean cancel(boolean mayInterruptIfRunning)

    試圖取消Future裏關聯的Callable任務。

  • V get()

    返回Callable裏call方法的返回值,直接調用該方法將導致程序阻塞,必須等到子線程結束時纔會得到返回值

  • V get(long timeout, TimeUnit unit)

    返回Callable裏call方法的返回值。該方法讓程序最多阻塞timeoutunit指定的時間。如果經過指定時間後Callable任務依然沒有返回值,將會拋出TimeoutException異常

  • boolean isCancelled

    如果在Callable任務正常完成前被取消則返回true

  • boolean isDone()

    如果Callable任務已經完成則返回true



使用Callable的步驟:

  1. 創建Callabe接口的實現類,並實現call方法。

    注意Callable接口有泛型限制,Callable接口裏的泛型形參類型與call方法返回值類型相同

  2. 創建Callable實現類的實例,使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象call方法的返回值

  3. 使用FutureTask對象作爲Thread對象的target來創建、啓動線程

  4. 調用FutureTask對象的方法來獲得子線程執行結束之後的返回值


例:

  1. 創建Callabe接口的實現類,並實現call方法。
package thread.Callable;

import java.util.concurrent.Callable;

public class RunThread implements Callable<Integer>{

    @Override
    public Integer call() {
        int i = 0;

        for (; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + " : i = " + i);
        }

        return i;

    }

}

測試類:

package thread.Callable;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableTest {

    public static void main(String[] args) {

        // 2.1 create instance of RunThread
        RunThread run = new RunThread();

        // 2.2 create instance of FutureTask 
        // and use it to wrap the instance of RunThread
        FutureTask<Integer> task = new FutureTask<Integer>(run);

        for (int j = 0; j < 30; j++) {
            System.out
                    .println(Thread.currentThread().getName() + " : j = " + j);

            if (j == 20) {

                try {
                    new Thread(task, "Vice Thread").start();
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }


        }
        try {
            System.out.println("return value of Vice Thread is : "
                    + task.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }


    }

}

程序最後調用FutureTask對象的get方法;來返回call方法的返回值,該方法將導致主線程被阻塞,知道call方法結束並返回爲止




5.7 Thread Pool(線程池)

系統啓動一個新線程的成本是比較高的,因爲它涉及與操作系統交互。在這種情況下,使用線程池可以很好的提高性能,尤其是當程序中需要創建大量生存期很短暫的線程時,更應該考慮使用線程池。

==線程池在系統啓動時就創建了大量空閒的線程,程序將一個Runnable對象傳給線程池,線程池就會啓動一條線程來執行該對象的run方法,當run方法執行結束之後,該線程不會死亡,而是再次返回線程池中成爲空閒狀態,等待執行下一個Runnable對象的run方法。==

使用線程池可以有效的控制系統中併發線程的數量,當系統中包含大量併發線程時會導致系統性能嚴重下降,甚至會導致JVM崩潰,而線程池的最大線程參數可以控制系統中併發線程不超過此數目。

java提供了一個Executors工廠類來生產線程池,包含下面幾個==靜態工廠方法==來創建線程池:

  • newCachedThreadPool()

    創建一個具有緩存功能的線程池,系統根據需要創建線程,這線程會被緩存在線程池中

  • newFixedThreadPool(int nThreads)

    創建一個可重用的、具有固定線程數的線程池

  • newSingleThreadExecutor()

    創建一個只有單線程的線程池,相當於newFixedThreadPool(int nThreads)傳入參數爲1

  • newScheduledThreadPool(int corePoolSize)

    • 創建具有固定線程數的線程池,可以在指定延遲後執行線程任務。

    • corePoolSize指池中所保存的線程數,即使線程是空閒的也被保存在線程池裏

  • newSingleThreadScheduledExecutor()

    創建只有固定線程的線程池,可以在指定延遲後執行線程任務。

前三個方法返回一個ExecutorService對象,該對象代表一個線程池,可以執行Runnable或Callable對象所代表的線程。

ExecutorService代表==儘快執行線程的線程池(只要池中有空閒線程就立即執行線程任務)==,程序只需要將一個Runnable或Callable對象提交給該線程池即可,該線程池就會盡快執行該任務(實現的細節對客戶解耦)。

ExecutorService提供了三個方法:

  • Future<?> submit(Runnable Task)

    • 將一個Runnable對象提交給制定的線程池,線程池在由空閒線程的時候立刻執行Runnable對象所代表的任務。

    • Future對象代表Runnable任務的返回值,但是run方法沒有返回值——所以Future對象在fun方法執行結束後返回null。*==但是可以調用Future的isDone,isCancelled方法來獲得Runnable對象的執行狀態。==*

  • <T> Future<T> submit(Runnable task, T result)

  • <T> Future<T> submit(Callable<T> task)

後兩個方法返回一個ScheduledExecutorService,是ExecutorService的子類,可以在指定延遲後執行線程任務。

ScheduledExecutorService代表==可以在制定延遲或週期性執行線程任務的線程池==,提供了以下四個方法:

  • <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit)

    指定的Callable任務將在delay後延遲執行

  • ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)

    指定的Runnable任務將在delay後執行

  • ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)

    指定的Runnable任務將在delay後執行,並且以設定頻率重複執行。
    (在intialDealy後開始執行,依次在initialDelay + period、nitialDelay + period * 2 …… 處重複執行)

  • ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)

    創建並執行一個在給定初始延遲後首次啓用的定期操作,隨後,==在每一次執行終止和下一次執行開始之間都存在給定的延遲。== 如果任務的任一此執行時遇到異常,就會取消後續執行。否則只能通過程序來顯式的取消或終止任務。

當用完一個線程池,應該調用該線程池的shutdowm()方法,該方法將啓動線程池的關閉序列,調用了shutdown()方法後的線程池==不再接受新任務,但會將以前所有已經提交的任務執行完成。當線程池中的所有任務都執行完成後,池中所有線程都會死亡==

另外也可以調用shutdownNow()方法來關閉線程池,該方法視圖停止所有正在執行的活動任務,暫停處理正在等待的任務,並返回等待執行的任務列表。(簡單粗暴。。。)


使用線程池來執行線程任務的步驟:

  1. 調用Executors類的靜態工廠方法創建一個ExecutorService對象,該對象代表一個線程池。

  2. 創建Runnable或Callable實現類的實例,作爲線程任務

  3. 調用ExecutorService對象的submit方法來提交Runnable或Callable任務

  4. 當不想再提交任何任務時調用ExecutorService對象的shutdown方法來關閉線程池

例:

package thread.threadPool;

public class RunTask implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + " : i = " + i);
        }
    }

}
package thread.threadPool;

import java.util.concurrent.Callable;

public class CallTask implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        int j = 0;
        for (; j < 20; j++) {
            System.out.println(Thread.currentThread().getName() + " : j = " + j);
        }
        return j;
    }

}
package thread.threadPool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestThreadPool {

    public static void main(String[] args) {

        // 1.create threadPool
        ExecutorService pool = Executors.newFixedThreadPool(6);

        // 2. create tasks
        // 3. submit tasks
        pool.submit(new CallTask());
        pool.submit(new RunTask());

        // 4. shut down the pool after execution
        pool.shutdown();


    }

}




5.8 ThreadLocal

通過使用ThreadLocal類可以簡化多線程編程時的併發訪問,使用這個工具類可以很簡潔的編寫出優美的多線程程序。

ThreadLocal,是ThreadLocalVariable的意思。功能是:==爲每一個使用該變量的線程都提供一個變量值的副本,使每一個線程都可以獨立的改變自己的副本,而不會和其他線程的副本發生衝突。==從每一個線程的角度來看,好像每一個線程都==完全擁有該變量==

ThreadLocal的用法非常簡單,提供了三個public方法:

  • T get()

    返回此線程局部變量中當前線程副本的值

  • void remove()

    刪除此線程局部變量中當前線程的值

  • void set(T value)

    設置此線程局部變量中當前線程副本的值

例:

package thread.threadLocal;

public class Account {

    private ThreadLocal<String> name = new ThreadLocal<String>();

    public Account(String name) {
        this.name.set(name);
        System.out.println("----" + this.name.get());
        // System.out.println(Thread.currentThread().getName());
    }

    public String getName() {
        return this.name.get();
    }

    public void setName(String name) {
        this.name.set(name);
    }

}
package thread.threadLocal;

public class MyThread extends Thread {

    private Account account;

    public MyThread(Account account, String name) {
        super(name);
        this.account = account;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (i == 6) {
                this.account.setName(this.getName());
            }

            System.out.println(this.account.getName() + " : i = " + i);
        }
    }

}

測試類:

package thread.threadLocal;

public class TestThreadLocal {

    public static void main(String[] args) {

        Account account = new Account("initAccount");

        new MyThread(account, "MyThread 1").start();;
        new MyThread(account, "MyThread 2").start();

    }

}

結果:

----initAccount----
null : i = 0
null : i = 1
null : i = 0
null : i = 2
null : i = 1
null : i = 3
null : i = 2
null : i = 4
null : i = 3
null : i = 5
null : i = 4
MyThread 1 : i = 6
null : i = 5
MyThread 1 : i = 7
MyThread 2 : i = 6
MyThread 2 : i = 7
MyThread 2 : i = 8
MyThread 2 : i = 9
MyThread 1 : i = 8
MyThread 1 : i = 9
  • 上面的代碼中,有兩條線程,只有一個賬戶,即只有一個賬戶名

  • 但由於賬戶名是ThreadLoacl類型,所以兩條線程雖然有同一個Account,但每條線程都各有一個賬戶名的副本,兩個副本毫無關係,互不影響

  • 所以從i == 6開始,將看到裏那個條線程訪問同一個賬戶時會有不同的賬戶名。

總結:

==ThreadLocal和其他所有的同步機制都是爲了解決多線程中對同一變量的訪問衝突。==

  • 在普通的同步機制中,是通過對對象加鎖的方式來實現對同一變量的安全訪問:

    • synchronized(obj)
    • 把方法用synchronized修飾成同步方法
    • 顯式使用Lock對象的lock和unlock方法加鎖、釋放鎖

    在這種情形下,該變量是多個線程共享的,所以需要和細緻的分析在什麼時候對變量進行讀寫,什麼時候需要鎖定某個對象,什麼時候需要釋放該對象的鎖。

    在這種情況下,==系統並沒有將這份同步資源複製多分,只是用了安全機制來控制對這份資源的訪問而已。==

  • ThreadLocal從另一個角度來解決多線程的併發訪問:**==ThreadLocal將需要併發訪問的資源複製出多份來,每個線程擁有一份資源,每一個線程都擁有自己獨立的資源副本,從而就沒有必要對該變量進行同步了。==**ThreadLocal提供了線程安全的共享對象,可以把不安全的整個變量封裝進ThreadLocal,或者把該對象與線程相關的狀態用ThreadLocal保存。

但是必須注意的是:ThreadLocal並不能替代同步機制!兩者面向的領域不同。

  • 同步機制是爲了同步多個線程對相同資源的併發訪問,是==多個線程之間進行通信==的有效方式。(想一下之前兩個人取同一個賬戶的錢,當一個取完800元之後,賬戶只剩下200元,另一個人相取800元會想提示取款失敗,這就涉及到了線程之間的通信)

  • 而ThreadLocal是==隔離多個線程的數據共享==(每個線程擁有一份獨立的副本,各個線程對自己的副本操作,而不是對同步資源操作),從根本上避免了多個線程之間共享資源(變量),從而也就不需要對多個線程進行同步了。

所以:

  • 如果需要進行多個線程之間共享資源,達到線程之間的通信功能 ——> 用==同步機制==

  • 如果僅僅需要隔離多個線程之間的共享衝突,可以用==ThreadLocal==



5.9 包裝線程不安全的集合

ArrayList、LinkedList、HashSet、HashMap等都是==線程不安全==的,也就是有可能當多個線程向這些集合放入一個元素時,可能會破壞集合數據的完整性。

可以使用Collections類提供的靜態方法來把這些集合包裝成線程安全的集合:

  • <T> Collection<T> synchronizedCollection(Collection<T> c)

    返回指定Collection對應的線程安全的Collection

  • static <T> List<T> synchronizedList(List<T> list)

    返回指定List對應的線程安全的List

  • static <K, V> Map<K, V> synchronizedMap(Map<K, V> m)

    返回指定Map對應的線程安全的Map

  • static <T> Set<T> synchronizedSet(Set<T> s)

    返回指定Set對應的線程安全的set

  • static <K, V> SortedMap<K, V> synchronizedSortedMap(SortedMap<K, V> m)

    返回指定SortedMap對應的線程安全的SortedMap

  • static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s)

    返回指定SortedSet對應的線程安全的SortedSet

==如果需要把某個集合包裝成線程安全的集合,應該在創建之後立即包裝!==

例:

HashMap m = Collections.sychronizedMap(new HashMap());



5.10 線程安全的集合類

JDK1.5後再java.util.concurrent包下提供了==ConcuurentHashMap====ConcurrentLinkedQueue==支持併發訪問的集合。

默認情況下都支持多條線程併發寫訪,這些寫入線程的所有操作都是線程安全的,但讀取操作不必鎖定。

當多個線程共享訪問一個公共集合時,==ConcurrentLinkedQueue==是一個合適的選擇:

  • 不允許使用null元素

  • 實現了多線程的高效訪問,多條線程訪問==ConcurrentLinkedQueue==時無需等待。

與HashMap和普通集合不同的是,ConcuurentHashMapConcurrentLinkedQueue支持多線程併發訪問,所以==當使用迭代器來遍歷元素時,該迭代器可能不能反應出創建迭代器之後做的修改,但程序不會拋異常。==

使用java.util包下的Collection作爲集合對象時,==如果該集合對象創建迭代器後集合元素髮生改變,將引發ConcurrentModificationException==

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