詳解多線程

一個任務一般就是一個程序,每一個運行中的程序就是一個進程。當一個程序運行時,內部可能包含了多個順序執行流,每一個順序執行流就是一個線程。
  java

進程

定義:

  當一個程序進入內存運行時,即變成一個進程。進程是處於運行過程當中的程序,而且具備必定的獨立功能,進程是系統進行資源分配和調度的一個獨立單位。面試

進程的特色:

  1. 獨立性:是系統獨立存在的實體,擁有本身獨立的資源,有本身私有的地址空間。在沒有通過進程自己容許的狀況下,一個用戶的進程不能夠直接訪問其餘進程的地址空間。算法

  2. 動態性:進程與程序的區別在於:程序只是一個靜態的指令集合,而進程是一個正在系統中活動的指令集和,進程中加入了時間的概念。進程具備本身的生命週期和不一樣的狀態,這些都是程序不具有的。數據庫

  3. 併發性:多個進程能夠在單個處理器上併發執行,多個進程之間不會相互影響。編程

  

並行性和併發性

  並行:指在同一時刻,有多條指令在多個處理上同時執行。(多核同時工做)數組

  併發:指在同一時刻只能有一條指令執行,但多個進程指令被快速輪換執行,使得在宏觀上具備多個進程同時執行的效果。(單核在工做,單核不停輪詢)緩存

  

線程

  多線程擴展了多進程的概念,使得同一個進程能夠同時併發處理多個任務。
  線程(Thread)也被成爲輕量級的進程,線程是進程執行的單元,線程在程序中是獨立的、併發的執行流安全

  當進程被初始化後,主線程就被建立了。絕大數應用程序只須要有一個主線程,但也能夠在進程內建立多條的線程,每一個線程也是相互獨立的。性能優化

  一個進程能夠擁有多個線程,一個線程必須有一個父進程。多線程

  線程能夠擁有本身的堆棧、本身的程序計數器和本身的局部變量,但不擁有系統資源,它與父進程的其餘線程共享該進程所擁有的所有資源,所以編程更加方便。

  線程是獨立運行的,它並不知道進程中是否還有其餘的線程存在。線程的執行是搶佔式的,即:當前運行的線程在任什麼時候候都有可能被掛起,以便另一個線程能夠運行。

  一個線程能夠建立和撤銷另外一個線程,同一個進程中多個線程之間能夠併發執行。

  線程的調度和管理由進程自己負責完成。

  概括而言:操做系統能夠同時執行多個任務,每一個任務就是進程;進程能夠同時執行多個任務,每一個任務就是線程

  

多線程的優勢:

  1. 進程之間不能共享內存,但線程之間共享內存很是容易

  2. 系統建立進程要爲該進程從新分配系統資源,但建立線程的代價則小得多。所以多線程實現多任務併發比多線程的效率高。

  3. Java語言內置了多線程功能支撐,簡化了多線程的編程。

  
  

線程的建立和啓動

1、繼承Thread類建立線程類

步驟:
① 定義Thread類的子類,並重寫該類的run()方法,該run()方法的方法體就表明了線程須要完成的任務,稱爲線程執行體

② 建立Thread子類的實例,即建立了線程對象

③ 調用線程對象的start()方法來啓動該線程
示例:

// 經過繼承Thread類來建立線程類
public class FirstThread extends Thread {
    private int i ;
    // 重寫run方法,run方法的方法體就是線程執行體
    public void run() {
        for ( ; i < 100 ; i++ )
        {
            // 當線程類繼承Thread類時,直接使用this便可獲取當前線程
            // Thread對象的getName()返回當前該線程的名字
            // 所以能夠直接調用getName()方法返回當前線程的名
            System.out.println(getName() +  " " + i);
        }
    }
    public static void main(String[] args) {
        for (int i = 0; i < 100;  i++)
        {
            // 調用Thread的currentThread方法獲取當前線程
            System.out.println(Thread.currentThread().getName() +  " " + i);
            if (i == 20)
            {
                // 建立、並啓動第一條線程
                new FirstThread().start();
                // 建立、並啓動第二條線程
                new FirstThread().start();
            }
        }
    }
}
複製代碼
注意點:

① 當Java程序開始運行後,程序至少會建立一個主線程,main()方法的方法體表明主線程的線程執行體

② 當線程類繼承Tread類時,直接使用this便可以獲取當前線程

③ 繼承Thread類建立線程類,多個線程之間沒法共享線程類的實例變量

  

2、實現Runnable接口建立線程類

步驟:

① 定義Runnable接口的實現類,並重寫該接口的run()方法

② 建立Runnable實現類的實例,並以此實例做爲Thread的target來建立Tread對象,該Tread對象纔是真正的線程對象

// 經過實現Runnable接口來建立線程類
public class SecondThread implements Runnable {
    private int i ;

    // run方法一樣是線程執行體
    public void run() {
        for ( ; i < 100 ; i++ )
        {
            // 當線程類實現Runnable接口時,
            // 若是想獲取當前線程,只能用Thread.currentThread()方法。
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100;  i++)
        {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 20)
            {
                SecondThread st = new SecondThread();     // ①

                // 經過new Thread(target , name)方法建立新線程
                new Thread(st , "新線程1").start();
                new Thread(st , "新線程2").start();
            }
        }
    }
}
複製代碼
注意點:

① 實現Runnable接口建立線程類,必須經過Thread.currentThread()方法來得到當前線程對象

② 實現Runnable接口建立線程類,多個線程能夠共享線程類的實例變量

  

3、使用Callable和Future建立線程

Callable接口提供了一個call()方法,call()方法比run()方法更強大:
① call()方法能夠由返回值

② call()方法能夠聲明拋出異常

步驟:

① 建立Callable接口的實現類,並實現call()方法,該call()方法做爲線程執行體,且該call()方法有返回值

② 使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方法的返回值

③ 調用FutureTask對象的get()方法得到子線程執行結束的返回值

示例:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//使用Callable接口和Future來建立線程
public class ThreadFuture {
    //拋出異常
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        //建立FutureTask對象,包裝 Callable接口實例
        FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{
            int sum = 0;
            for(int i = 0;i<100;i++){
                System.out.println(Thread.currentThread().getName()+":"+i);
                sum += i;
            }
            //注意看這裏有返回值
            return sum;
        });
        //使用task做爲 Thread類的target 來建立一個線程
        Thread instance = new Thread(task);
        //啓動線程
        instance.start();
        //sleep一段時間,讓上面的線程執行完畢
        Thread.sleep(1000);
        
        //這裏能夠調用task.get() 獲取上面的那個線程的返回值
        System.out.println("線程返回值:"+task.get());
        
    }
}
複製代碼

  
  

建立線程三種方式的對比:

實現Runnable接口、Callable接口建立線程

優勢:
①實現的是接口,還能夠繼承其餘類

② 多個線程能夠共享同一個target對象,適合多個相同的線程來處理同一份資源的狀況

缺點:
① 編程稍微複雜

② 獲取當前線程必須用Thread.currentThread()方法來得到

繼承Tread類建立線程

優勢:
①編程簡單

② 獲取當前線程,能夠直接使用this來得到

缺點:
① 已經繼承了Thread類,不能繼承其餘類

  
  

線程的生命週期

線程的生命週期要經歷新建(New)、就緒(Runnable)、運行(Running)、阻塞(Blocke)和死亡(Dead)5種狀態。

尤爲是當線程啓動之後,它不可能一直「霸佔」着CPU獨自運行,因此CPU須要在多條線程之間切換,因而線程狀態也會屢次在運行、阻塞之間切換。

一、新建和就緒狀態

當程序使用new關鍵字建立了一個線程以後,該線程就處於新建狀態,此時它僅僅由Java虛擬機爲其分配內存,而且初始化其成員變量的值。此時的線程對象沒有表現出任何線程隊動態特徵,程序也不會執行線程的線程執行體。

當線程對象調用了start()方法以後,該線程處於就緒狀態,Java虛擬機會爲其建立方法調用棧和程序計數器,處於這個狀態中的線程並無開始運行,只是表示該線程能夠運行了,至於該線程什麼時候開始運行,取決於JVM裏線程調度器的調度。

tips:

  • 啓動線程使用start()方法,而不是run()方法,若是調用run()方法,則run()方法當即就會被執行,並且在run()方法返回以前,其餘線程沒法併發執行,也就是說,若是直接調用線程對象的run()方法,系統把線程對象當成一個普通對象,而run()方法也是一個普通方法,而不是線程執行體。

  • 若是直接調用線程對象的run()方法,則run()方法裏不能直接經過getName()方法來得到當前執行線程的名字,而是須要使用Thread.currentThread()方法先得到當前線程,再調用線程對象的getName()方法來得到線程的名字。啓動線程的正確方法是調用Thread對象的start()方法,而不是直接調用run()方法,不然就變成單線程程序了。

  • 調用了線程的run()方法以後,該線程已經再也不處於新建狀態,不要再次調用線程對象的start()方法。

二、運行和阻塞狀態

若是處於就緒狀態的線程得到了CPU,開始執行run()方法的線程執行體,則該線程處於運行狀態。

但線程不可能一直處於運行狀態,它在運行過程當中會被中斷,從而進入一個阻塞的狀態

當發生以下狀況時,線程將會進入阻塞狀態:

一、線程調用sleep()方法主動放棄所佔用的處理器資源。

二、線程調用了一個阻塞式IO方法,在該方法返回以前,該線程被阻塞。

三、線程試圖得到一個同步監視器,但該同步監視器正被其餘線程所持有。

四、線程在等待某個通知(notify)。

五、程序調用了線程的suspend()方法將該線程掛起。但這個方法容易致使死鎖,因此應該儘可能避免使用該方法。

針對上面幾種狀況,當發生以下特定的狀況時能夠解除上面的阻塞,讓該線程從新進入就緒狀態。

一、調用sleep()方法的線程通過了指定時間。

二、線程調用的阻塞式IO方法已經返回。

三、 線程成功地得到了試圖取得的同步監視器。

四、 線程正在等待某個通知時,其餘線程發出了一個通知。

五、處於掛起狀態的線程被調用了resume()恢復方法。

線程狀態轉化圖

從圖中能夠看出,線程從阻塞狀態只能進入就緒狀態,沒法直接進入運行狀態。

而就緒和運行狀態之間的轉換一般不受程序控制,而是由系統線程調度所決定。

當處於就緒狀態的線程得到處理器資源時,該線程進入運行狀態;當處於運行狀態的線程失去處理器資源時,該線程進入就緒狀態。

但有一個方法例外,調用yield()方法可讓運行狀態的線程轉入就緒狀態。

線程死亡

線程會以以下三種方式結束,結束後就處於死亡狀態。

  • run()call()方法執行完成,線程正常結束。

  • 線程拋出一個未捕獲的ExceptionError

  • 直接調用該線程的stop()方法來結束該線程——該方法容易致使死鎖,一般不推薦使用。

tips:

一、當主線程結束時,其餘線程不受任何影響,並不會隨之結束。一旦子線程啓動起來後,它就擁有和主線程相同的地位,它不會受主線程的影響。

二、爲了測試某個線程是否已經死亡,能夠調用線程對象的isAlive()方法,當線程處於就緒、運行、阻塞三種狀態時,該方法將返回true;當線程處於新建、死亡兩種狀態時,該方法將返回false

三、不要試圖對一個已經死亡的線程調用start()方法使它從新啓動,死亡就是死亡,該線程將不可再次做爲線程執行。在線程已經死亡的狀況下再次調用start()方法將會引起IIIegalThreadException異常。

四、不能對死亡的線程調用start()方法,程序只能對新建狀態的線程調用start()方法,對新建的線程兩次調用start()方法也是錯誤的,會引起IIIegalThreadStateException異常。

  

控制線程

一、join線程

Thread提供了讓一個線程等待另外一個線程完成的方法:join()方法。當在某個程序執行流中調用其餘線程的join()方法時,調用線程將被阻塞,直到被join()方法加入的join線程執行完爲止。

join()方法一般由使用線程的程序調用,以將大問題劃分紅許多小問題,每一個小問題分配一個線程。當全部的小問題都獲得處理後,再調用主線程來進一步操做。

代碼示例:

public class JoinThread extends Thread {
    // 提供一個有參數的構造器,用於設置該線程的名字
    public JoinThread(String name) {
        super(name);
    }
    // 重寫run()方法,定義線程執行體
    public void run() {
        for (int i = 0; i < 100 ; i++ )
        {
            System.out.println(getName() + " " + i);
        }
    }
    public static void main(String[] args)throws Exception {
        // 啓動子線程
        new JoinThread("新線程").start();
        for (int i = 0; i < 100 ; i++ )
        {
            if (i == 20)
            {
                JoinThread jt = new JoinThread("被Join的線程");
                jt.start();
                // main線程調用了jt線程的join()方法,main線程必須等jt執行結束纔會向下執行
                jt.join();
            }
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}
複製代碼

二、後臺線程

有一種線程,它是在後臺運行的,它的任務是爲其餘的線程提供服務,這種線程被稱爲「後臺線程(Daemon Thread)」,又稱爲「守護線程」或「精靈線程」。JVM的垃圾回收線程就是典型的後臺線程。

後臺線程有個特徵:若是全部的前臺線程都死亡,後臺線程會自動死亡。

調用Thread對象的setDaemon(true)方法可將指定線程設置成後臺線程。

tips:
一、Thread類還提供了一個isDaemon()方法,用於判斷指定線程是否爲後臺線程。

二、前臺線程建立的子線程默認是前臺線程,後臺線程子線程默認是後臺線程。

三、前臺線程死亡後,JVM會通知後臺線程死亡,但從它接收指令到作出響應,須要必定時間。

並且要將某個線程設置爲後臺線程,必須在該線程啓動以前設置,也就是說,setDaemon(true)必須在start()方法以前調用,不然會引起llegalThreadStateException異常。

三、線程睡眠:sleep

若是須要讓當前正在執行的線程暫停一段時間,並進入阻塞狀態,則能夠經過調用Thread類的靜態sleep()方法來實現。

四、線程讓步yield()

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

yield()只是讓當前線程暫停一下,讓系統的線程調度器從新調度一次,徹底可能的狀況是:當某個線程調用了yield()方法暫停以後,線程調度器又將其調度出來從新執行。

實際上,當某個線程調用了yield()方法暫停以後,只有優先級與當前線程相同,或者優先級比當前線程更高的處於就緒狀態的線程纔會得到執行的機會。

關於sleep()方法和yield()方法的區別以下

  1. sleep()方法暫停當前線程後,會給其餘線程執行機會,不會理會其餘線程的優先級;但yield()方法只會給優先級相同,或優先級更高的線程執行機會。

  2. sleep()方法會將線程轉入阻塞狀態,直到通過阻塞時間纔會轉入就緒狀態;而yield()不會將線程轉入阻塞狀態,它只是強制當前線程進入就緒狀態。所以徹底有可能某個線程調用yield()方法暫停以後,當即再次得到處理器資源被執行。

  3. sleep()方法聲明拋出了InterruptedException 異常,因此調用sleep()方法時要麼捕捉該異常,要麼顯式聲明拋出該異常;而yield()方法則沒有聲明拋出任何異常。

  4. sleep()方法比yield()方法有更好的可移植性,一般不建議使用yield()方法來控制併發線程的執行。

五、改變線程優先級

經過Thread類提供的setPriority(int newPriority)getPriority()方法來設置和返回指定線程的優先級。

setPriority()方法的參數能夠是一個整數,範圍是1~10之間,也可使用Thread類的以下三個靜態常量。

MAXPRIORITY:其值是10。

MIN PRIORITY:其值是1。

NORM_PRIORITY:其值是5。

  

線程同步

爲了解決多個線程訪問同一個數據時,會出現問題,所以須要進行線程同步。就像前面介紹的文件併發訪問,當有兩個進程併發修改同一個文件時就有可能形成異常。

一、同步代碼塊

爲了解決線程同步問題,Java的多線程支持引入了同步監視器來解決這個問題,使用同步監視器的通用方法就是同步代碼塊。同步代碼塊的語法格式以下:

synchronized(obj)
  {.....
        //此處的代碼就是同步代碼塊
   }
複製代碼

上面語法格式中synchronized後括號裏的obj就是同步監視器,上面代碼的含義是:線程開始執行同步代碼塊以前,必須先得到對同步監視器的鎖定。

任什麼時候刻只能有一個線程能夠得到對同步監視器的鎖定,當同步代碼塊執行完成後,該線程會釋放對該同步監視器的鎖定。

一般推薦使用可能被併發訪問的共享資源充當同步監視器,代碼示例以下:

public class DrawThread extends Thread {
    // 模擬用戶帳戶
    private Account account;
    // 當前取錢線程所但願取的錢數
    private double drawAmount;
    public DrawThread(String name , Account account , double drawAmount) {
        super(name);
        this.account = account;
        this.drawAmount = drawAmount;
    }
    // 當多條線程修改同一個共享數據時,將涉及數據安全問題。
    public void run() {
        // 使用account做爲同步監視器,任何線程進入下面同步代碼塊以前,
        // 必須先得到對account帳戶的鎖定——其餘線程沒法得到鎖,也就沒法修改它
        // 這種作法符合:「加鎖 → 修改 → 釋放鎖」的邏輯
        synchronized (account)
        {
            // 帳戶餘額大於取錢數目
            if (account.getBalance() >= drawAmount)
            {
                // 吐出鈔票
                System.out.println(getName()
                    + "取錢成功!吐出鈔票:" + drawAmount);
                try
                {
                    Thread.sleep(1);
                }
                catch (InterruptedException ex)
                {
                    ex.printStackTrace();
                }
                // 修改餘額
                account.setBalance(account.getBalance() - drawAmount);
                System.out.println("\t餘額爲: " + account.getBalance());
            }
            else
            {
                System.out.println(getName() + "取錢失敗!餘額不足!");
            }
        }
        // 同步代碼塊結束,該線程釋放同步鎖
    }
}
複製代碼

  

二、同步方法

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

對於synchronized修飾的實例方法(非static方法)而言,無須顯式指定同步監視器,同步方法的同步監視器是this,也就是調用該方法的對象。

經過使用同步方法能夠很是方便地實現線程安全的類,線程安全的類具備以下特徵。

  1. 該類的對象能夠被多個線程安全地訪問。

  2. 每一個線程調用該對象的任意方法以後都將獲得正確結果。

  3. 每一個線程調用該對象的任意方法以後,該對象狀態依然保持合理狀態。

代碼示例:

public class Account {
    // 封裝帳戶編號、帳戶餘額兩個成員變量
    private String accountNo;
    private double balance;
    public Account(){}
    // 構造器
    public Account(String accountNo , double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    // accountNo的setter和getter方法
    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }
    public String getAccountNo() {
        return this.accountNo;
    }
    // 所以帳戶餘額不容許隨便修改,因此只爲balance提供getter方法,
    public double getBalance() {
        return this.balance;
    }

    // 提供一個線程安全draw()方法來完成取錢操做
    public synchronized void draw(double drawAmount) {
        // 帳戶餘額大於取錢數目
        if (balance >= drawAmount)
        {
            // 吐出鈔票
            System.out.println(Thread.currentThread().getName()
                + "取錢成功!吐出鈔票:" + drawAmount);
            try
            {
                Thread.sleep(1);
            }
            catch (InterruptedException ex)
            {
                ex.printStackTrace();
            }
            // 修改餘額
            balance -= drawAmount;
            System.out.println("\t餘額爲: " + balance);
        }
        else
        {
            System.out.println(Thread.currentThread().getName()
                + "取錢失敗!餘額不足!");
        }
    }

    // 下面兩個方法根據accountNo來重寫hashCode()和equals()方法
    public int hashCode() {
        return accountNo.hashCode();
    }
    public boolean equals(Object obj) {
        if(this == obj)
            return true;
        if (obj !=null
            && obj.getClass() == Account.class)
        {
            Account target = (Account)obj;
            return target.getAccountNo().equals(accountNo);
        }
        return false;
    }
}
複製代碼

  
上面程序中增長了一個表明取錢的draw()方法,並使用了synchronized關鍵字修飾該方法,把該方法變成同步方法。

該同步方法的同步監視器是this,所以對於同一個Account帳戶而言,任意時刻只能有一個線程得到對Account對象的鎖定,而後進入draw()方法執行取錢操做,這樣也能夠保證多個線程併發取錢的線程安全。

三、釋放同步監視器的鎖定

程序沒法顯式釋放對同步監視器的鎖定,線程會在以下狀況下釋放對同步監視器的鎖定。

  • 當前線程的同步方法、同步代碼塊執行結束,當前線程即釋放同步監視器。

  • 當前線程在同步代碼塊、同步方法中遇到breakreturn終止了該代碼塊、該方法的繼續執行,當前線程將會釋放同步監視器。

  • 當前線程在同步代碼塊、同步方法中出現了未處理的Error 或Exception,致使了該代碼塊、該方法異常結束時,當前線程將會釋放同步監視器。

  • 當前線程執行同步代碼塊或同步方法時,程序執行了同步監視器對象的wait0方法,則當前線程暫停,並釋放同步監視器。

  
在以下所示的狀況下,線程不會釋放同步監視器:

  • 線程執行同步代碼塊或同步方法時,程序調用Thread.sleep()Thread.yield()方法來暫停當前線程的執行,當前線程不會釋放同步監視器。

  • 線程執行同步代碼塊時,其餘線程調用了該線程的suspend()方法將該線程掛起,該線程不會釋放同步監視器。固然,程序應該儘可能避免使用suspend()resume()方法來控制線程。

四、同步鎖(Lock)

Lock、ReadWriteLock是Java5提供的兩個根接口,併爲Lock提供ReentrantLock(可重入鎖)實現類,爲ReadWriteLock提供了ReentrantReadWriteLock 實現類。

Java8新增了新型的StampedLock類,在大多數場景中它能夠替代傳統的ReentrantReadWriteLock。

ReentrantReadWriteLock爲讀寫操做提供了三種鎖模式:Writing、ReadingOptimistic、Reading。

在實現線程安全的控制中,比較經常使用的是ReentrantLock(可重入鎖)。使用該Lock對象能夠顯式地加鎖、釋放鎖,一般使用ReentrantLock的代碼格式以下:

public class Account {
    // 定義鎖對象
    private final ReentrantLock lock = new ReentrantLock();
    // 封裝帳戶編號、帳戶餘額的兩個成員變量
    private String accountNo;
    private double balance;
    public Account(){}
    // 構造器
    public Account(String accountNo , double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    // accountNo的setter和getter方法
    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }
    public String getAccountNo() {
        return this.accountNo;
    }
    // 所以帳戶餘額不容許隨便修改,因此只爲balance提供getter方法,
    public double getBalance() {
        return this.balance;
    }

    // 提供一個線程安全draw()方法來完成取錢操做
    public void draw(double drawAmount) {
        // 加鎖
        lock.lock();
        try
        {
            // 帳戶餘額大於取錢數目
            if (balance >= drawAmount)
            {
                // 吐出鈔票
                System.out.println(Thread.currentThread().getName()
                    + "取錢成功!吐出鈔票:" + drawAmount);
                try
                {
                    Thread.sleep(1);
                }
                catch (InterruptedException ex)
                {
                    ex.printStackTrace();
                }
                // 修改餘額
                balance -= drawAmount;
                System.out.println("\t餘額爲: " + balance);
            }
            else
            {
                System.out.println(Thread.currentThread().getName()
                    + "取錢失敗!餘額不足!");
            }
        }
        finally
        {
            // 修改完成,釋放鎖
            lock.unlock();
        }
    }

    // 下面兩個方法根據accountNo來重寫hashCode()和equals()方法
    public int hashCode() {
        return accountNo.hashCode();
    }
    public boolean equals(Object obj) {
        if(this == obj)
            return true;
        if (obj !=null
            && obj.getClass() == Account.class)
        {
            Account target = (Account)obj;
            return target.getAccountNo().equals(accountNo);
        }
        return false;
    }
}
複製代碼

  
同步方法或同步代碼塊使用與競爭資源相關的、隱式的同步監視器,而且強制要求加鎖和釋放鎖要出如今一個塊結構中,並且當獲取了多個鎖時,它們必須以相反的順序釋放,且必須在與全部鎖被獲取時相同的範圍內釋放全部鎖。

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

ReentrantLock鎖具備可重入性,也就是說,一個線程能夠對已被加鎖的ReentrantLock鎖再次加鎖,ReentrantLock對象會維持一個計數器來追蹤lock()方法的嵌套調用,線程在每次調用lock()加鎖後,必須顯式調用unlock()來釋放鎖,因此一段被鎖保護的代碼能夠調用另外一個被相同鎖保護的方法。

死鎖

當兩個線程相互等待對方釋放同步監視器時就會發生死鎖一旦出現死鎖,整個程序既不會發生任何異常,也不會給出任何提示,只是全部線程處於阻塞狀態,沒法繼續。

死鎖示例:

有兩個類 A 和 B ,這兩個類每一個類都各含有兩個同步方法,利用兩個線程來進行操做。

首先線程1調用 A 類的同步方法 A1,而後休眠,此時線程2會開始工做,它會調用 B 類的同步方法 B1,而後也休眠。

此時線程1休眠結束,它繼續執行方法 A1 ,A1的下一步操做是調用 B 中的同步方法 B2,由於此時 B 的對象示例正被線程2所佔據,所以線程1只能等待對 B 的鎖的釋放。

此時線程2又甦醒了,它繼續執行方法 B1,B1的下一步操做是調用 A 中的同步方法 A2,所以是 A 類的對象也被線程1給鎖住了,所以線程2也只能等待,這樣就形成了線程1和線程2相互等待,從而致使了死鎖的發生。

代碼示例:

//A類
class A {
    public synchronized void foo( B b ) {
        System.out.println("當前線程名: " + Thread.currentThread().getName()
            + " 進入了A實例的foo()方法" );     // ①
        try
        {
            Thread.sleep(200);
        }
        catch (InterruptedException ex)
        {
            ex.printStackTrace();
        }
        System.out.println("當前線程名: " + Thread.currentThread().getName()
            + " 企圖調用B實例的last()方法");    // ③
        b.last();
    }
    public synchronized void last() {
        System.out.println("進入了A類的last()方法內部");
    }
}

//B類
class B {
    public synchronized void bar( A a ) {
        System.out.println("當前線程名: " + Thread.currentThread().getName()
            + " 進入了B實例的bar()方法" );   // ②
        try
        {
            Thread.sleep(200);
        }
        catch (InterruptedException ex)
        {
            ex.printStackTrace();
        }
        System.out.println("當前線程名: " + Thread.currentThread().getName()
            + " 企圖調用A實例的last()方法");  // ④
        a.last();
    }
    public synchronized void last() {
        System.out.println("進入了B類的last()方法內部");
    }
}

//線程類
public class DeadLock implements Runnable {
    A a = new A();
    B b = new B();
    public void init() {
        Thread.currentThread().setName("主線程");
        // 調用a對象的foo方法
        a.foo(b);
        System.out.println("進入了主線程以後");
    }
    public void run() {
        Thread.currentThread().setName("副線程");
        // 調用b對象的bar方法
        b.bar(a);
        System.out.println("進入了副線程以後");
    }
    
    //主函數
    public static void main(String[] args) {
        DeadLock dl = new DeadLock();
        // 以dl爲target啓動新線程
        new Thread(dl).start();
        // 調用init()方法
        dl.init();
    }
}
複製代碼

  

線程通訊

一、傳統的線程通訊——經過Object類提供的方法實現

藉助於Object類提供的wait()notify()notifyAll()三個方法。

這三個方法並不屬於Thread類,而是屬於Object類。但這三個方法必須由同步監視器對象來調用,這可分紅如下兩種狀況。

  • 對於使用synchronized修飾的同步方法,由於該類的默認實例(this)就是同步監視器,因此能夠在同步方法中直接調用這三個方法。

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

關於這三個方法的解釋以下:

  • wait():致使當前線程等待,直到其餘線程調用該同步監視器的notify()方法或notifyAll()方法來喚醒該線程。

  • notify():喚醒在此同步監視器上等待的單個線程。若是全部線程都在此同步監視器上等待,則會選擇喚醒其中一個線程。選擇是任意性的。只有當前線程放棄對該同步監視器的鎖定後(使用wait()方法),才能夠執行被喚醒的線程。

  • notifyAll:喚醒在此同步監視器上等待的全部線程。只有當前線程放棄對該同步監視器的鎖定後,才能夠執行被喚醒的線程。

使用Condition控制線程通訊

若是程序不使用synchronized 關鍵字來保證同步,而是直接便用Lock對象採保證同步,則系統中下存在隱式的同步監視器,也就不能使用wait()notify()notifyAll()方法進行線程通訊了。

當使用Lock 對象來保證同步時,Java提供了一個Condition類來保持協調,使用Condition可讓那些已經獲得Lock對象卻沒法繼續執行的線程釋放Lock對象,Condition對象也能夠喚醒其餘處於等待的線程。

Condition實例被綁定在一個Lock對象上。要得到特定Lock實例的Condition實例,調用Lock對象的newCondition()方法便可。Condition類提供了以下三個方法:

  • await():相似於隱式同步監視器上的wait()方法,致使當前線程等待,直到其餘線程調用該Conditionsignal()方法或signalAll()方法來喚醒該線程。

  • signal():喚醒在此Lock對象上等待的單個線程。若是全部線程都在該Lock對象上等待,則會選擇喚醒其中一個線程。選擇是任意性的。只有當前線程放棄對該Lock對象的鎖定後(使用await()方法),才能夠執行被喚醒的線程。

  • signalAIl():喚醒在此Lock對象上等待的全部線程。只有當前線程放棄對該Lock對象的鎖定後,才能夠執行被喚醒的線程。

public class Account {
    // 顯式定義Lock對象
    private final Lock lock = new ReentrantLock();
    // 得到指定Lock對象對應的Condition
    private final Condition cond  = lock.newCondition();
    // 封裝帳戶編號、帳戶餘額的兩個成員變量
    private String accountNo;
    private double balance;
    // 標識帳戶中是否已有存款的旗標
    private boolean flag = false;

    public Account(){}
    // 構造器
    public Account(String accountNo , double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    // accountNo的setter和getter方法
    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }
    public String getAccountNo() {
        return this.accountNo;
    }
    // 所以帳戶餘額不容許隨便修改,因此只爲balance提供getter方法,
    public double getBalance() {
        return this.balance;
    }

    public void draw(double drawAmount) {
        // 加鎖
        lock.lock();
        try
        {
            // 若是flag爲假,代表帳戶中尚未人存錢進去,取錢方法阻塞
            if (!flag)
            {
                cond.await();
            }
            else
            {
                // 執行取錢
                System.out.println(Thread.currentThread().getName()
                    + " 取錢:" +  drawAmount);
                balance -= drawAmount;
                System.out.println("帳戶餘額爲:" + balance);
                // 將標識帳戶是否已有存款的旗標設爲false。
                flag = false;
                // 喚醒其餘線程
                cond.signalAll();
            }
        }
        catch (InterruptedException ex)
        {
            ex.printStackTrace();
        }
        // 使用finally塊來釋放鎖
        finally
        {
            lock.unlock();
        }
    }
    public void deposit(double depositAmount) {
        lock.lock();
        try
        {
            // 若是flag爲真,代表帳戶中已有人存錢進去,則存錢方法阻塞
            if (flag)             // ①
            {
                cond.await();
            }
            else
            {
                // 執行存款
                System.out.println(Thread.currentThread().getName()
                    + " 存款:" +  depositAmount);
                balance += depositAmount;
                System.out.println("帳戶餘額爲:" + balance);
                // 將表示帳戶是否已有存款的旗標設爲true
                flag = true;
                // 喚醒其餘線程
                cond.signalAll();
            }
        }
        catch (InterruptedException ex)
        {
            ex.printStackTrace();
        }
        // 使用finally塊來釋放鎖
        finally
        {
            lock.unlock();
        }
    }

    // 下面兩個方法根據accountNo來重寫hashCode()和equals()方法
    public int hashCode() {
        return accountNo.hashCode();
    }
    public boolean equals(Object obj) {
        if(this == obj)
            return true;
        if (obj !=null
            && obj.getClass() == Account.class)
        {
            Account target = (Account)obj;
            return target.getAccountNo().equals(accountNo);
        }
        return false;
    }
}
複製代碼

  

使用阻塞隊列(BlockingQueue)控制線程通訊

Java5提供了一個BlockingQueue接口,雖然BlockingQueue也是Queue的子接口,但它的主要用途並非做爲容器,而是做爲線程同步的工具。

BlockingQueue具備一個特徵:

當生產者線程試圖向BlockingOueue中放入元素時,若是該隊列已滿,則該線程被阻塞;

當消費者線程試圖從BlockingQueue中取出元素時,若是該隊列已空,則該線程被阻塞。

BlockingQueue提供以下兩個支持阻塞的方法。

  • put(E e):嘗試把E元素放入BlockingQueue中,若是該隊列的元素已滿,則阻塞該線程。

  • take():嘗試從BlockingQueue的頭部取出元素,若是該隊列的元素已空,則阻塞該線程。

BlockingQueue繼承了Queue接口,固然也可以使用Queue接口中的方法。這些方法概括起來可分爲以下三組。

  • 在隊列尾部插入元素。包括add(E e)offer(E e)put(Ee)方法,當該隊列已滿時,這三個方法分別會拋出異常、返回false、阻塞隊列。

  • 在隊列頭部刪除並返回刪除的元素。包括remove()poll()take()方法。當該隊列已空時,這三個方法分別會拋出異常、返回false、阻塞隊列。

  • 在隊列頭部取出但不刪除元素。包括element()peek()方法,當隊列已空時,這兩個方法分別拋出異常、返回false。

使用阻塞隊列(BlockingQueue)來實現線程通訊,以消費者生產者爲例:

//生產者類
class Producer extends Thread {
    private BlockingQueue<String> bq;
    public Producer(BlockingQueue<String> bq) {
        this.bq = bq;
    }
    public void run() {
        String[] strArr = new String[]
        {
            "Java",
            "Struts",
            "Spring"
        };
        for (int i = 0 ; i < 999999999 ; i++ )
        {
            System.out.println(getName() + "生產者準備生產集合元素!");
            try
            {
                Thread.sleep(200);
                // 嘗試放入元素,若是隊列已滿,線程被阻塞
                bq.put(strArr[i % 3]);
            }
            catch (Exception ex){ex.printStackTrace();}
            System.out.println(getName() + "生產完成:" + bq);
        }
    }
}

//消費者類
class Consumer extends Thread {
    private BlockingQueue<String> bq;
    public Consumer(BlockingQueue<String> bq) {
        this.bq = bq;
    }
    public void run() {
        while(true)
        {
            System.out.println(getName() + "消費者準備消費集合元素!");
            try
            {
                Thread.sleep(200);
                // 嘗試取出元素,若是隊列已空,線程被阻塞
                bq.take();
            }
            catch (Exception ex){ex.printStackTrace();}
            System.out.println(getName() + "消費完成:" + bq);
        }
    }
}

//主程序
public class BlockingQueueTest2 {
    public static void main(String[] args) {
        // 建立一個容量爲1的BlockingQueue
        BlockingQueue<String> bq = new ArrayBlockingQueue<>(1);
        // 啓動3條生產者線程
        new Producer(bq).start();
        new Producer(bq).start();
        new Producer(bq).start();
        // 啓動一條消費者線程
        new Consumer(bq).start();
    }
}
複製代碼

  

線程池

系統啓動一個新線程的成本是比較高的,由於它涉及與操做系統交互。在這種情形下,使用線程池能夠很好地提升性能,尤爲是當程序中須要建立大量生存期很短暫的線程時,更應該考慮使用線程池。

與數據庫鏈接池相似的是,線程池在系統啓動時即建立大量空閒的線程,程序將一個Runnable對象或Callable對象傳給線程池,線程池就會啓動一個線程來執行它們的run()call()方法。

run()call()方法執行結束後,該線程並不會死亡,而是再次返回線程池中成爲空閒狀態,等待執行下一個Runnable對象的run()call()方法。

除此以外,使用線程池能夠有效地控制系統中併發線程的數量,當系統中包含大量併發線程時,會致使系統性能劇烈降低,甚至致使JVM崩潰,而線程池的最大線程數參數能夠控制系統中併發線程數不超過此數。

建立線程池

在Java5之前,開發者必須手動實現本身的線程池;從Java5開始,Java內建支持線程池。

Java5新增了一個Executors工廠類來產生線程池,該工廠類包含以下幾個靜態工廠方法來建立線程池。

  • newCachedThreadPool():建立一個具備緩存功能的線程池,系統根據須要建立線程,這些線程將會被緩存在線程池中。

  • newFixedThreadPool(int nThreads):建立一個可重用的、具備固定線程數的線程池。

  • newSingle ThreadExecutor():建立一個只有單線程的線程池,它至關於調用newFixedThread Pool()方法時傳入參數爲1。

  • newScheduledThreadPool(int corePoolSize):建立具備指定線程數的線程池,它能夠在指定延遲後執行線程任務。corePoolSize指池中所保存的線程數,即便線程是空閒的也被保存在線程池內。

  • newSingle ThreadScheduledExecutor):建立只有一個線程的線程池,它能夠在指定延遲後執行線程任務。

  • ExecutorService new WorkStealingPool(int parallelism):建立持有足夠的線程的線程池來支持給定的並行級別,該方法還會使用多個隊列來減小競爭。

  • ExecutorService new WorkStealingPool):該方法是前一個方法的簡化版本。若是當前機器有4個CPU,則目標並行級別被設置爲4,也就是至關於爲前一個方法傳入4做爲參數。

上面7個方法中的前三個方法返回一個ExecutorService對象,該對象表明一個線程池,它能夠執行Runnable對象或Callable對象所表明的線程;

而中間兩個方法返回一個ScheduledExecutorService線程池,它是ExecutorService的子類,它能夠在指定延遲後執行線程任務;

最後兩個方法則是Java8新增的,這兩個方法可充分利用多CPU並行的能力。這兩個方法生成的work stealing池,都至關於後臺線程池,若是全部的前臺線程都死亡了,work stealing池中的線程會自動死亡。

ExecutorService表明儘快執行線程的線程池(只要線程池中有空閒線程,就當即執行線程任務)

程序只要將一個Runnable對象或Callable對象(表明線程任務)提交給該線程池,該線程池就會盡快執行該任務。

ExecutorService裏提供了以下三個方法。

  • Future<?>submit(Runnable task):將一個Runnable對象提交給指定的線程池,線程池將在有空閒線程時執行Runnable對象表明的任務。其中Future對象表明Runnable任務的返回值,但run()方法沒有返回值,因此Future對象將在run()方法執行結束後返回null

但能夠調用FutureisDone()isCancelled()方法來得到Runnable對象的執行狀態。

  • <T>Future-T>submit(Runnable task,T result):將一個Runnable對象提交給指定的線程池,線程池將在有空閒線程時執行Runnable對象表明的任務。其中result顯式指定線程執行結束後的返回值,因此Future對象將在run()方法執行結束後返回result

  • <T>Future-T>submit(Callable<T>task):將一個Callable對象提交給指定的線程池,線程池將在有空閒線程時執行Callable對象表明的任務。其中Future表明Callable對象裏call()方法的返回值。

ScheduledExecutorService表明可在指定延遲後或週期性地執行線程任務的線程池,它提供了以下4個方法。

  • ScheduledFuture<V> schedule(Callable-V> callable,long delay,TimeUnit unit):指定callable任務將在delay延遲後執行。

  • ScheduledFuture<?>schedule(Runnable command,long delay,TimeUnit unit):指定command任務將在delay延遲後執行。

  • ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit):指定command任務將在delay延遲後執行,並且以設定頻率重複執行。也就是說,在initialDelay後開始執行,依次在initialDelay+period、initialDelay+2*period…處重複執行,依此類推。

  • ScheduledFuture<?>scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit):建立並執行一個在給定初始延遲後首次啓用的按期操做,隨後在每一次執行終止和下一次執行開始之間都存在給定的延遲。若是任務在任一次執行時遇到異常,就會取消後續執行;不然,只能經過程序來顯式取消或終止該任務。

用完一個線程池後,應該調用該線程池的shutdown0方法,該方法將啓動線程池的關閉序列,調用shutdown()方法後的線程池再也不接收新任務,但會將之前全部已提交任務執行完成。當線程池中的全部任務都執行完成後,池中的全部線程都會死亡;

另外也能夠調用線程池的shutdownNow()方法來關閉線程池,該方法試圖中止全部正在執行的活動任務,暫停處理正在等待的任務,並返回等待執行的任務列
表。

使用線程池來執行線程任務的步驟以下。

①調用Executors類的靜態工廠方法建立一個ExecutorService對象,該對象表明一個線程池。

②建立Runnable 實現類或Callable實現類的實例,做爲線程執行任務。

③調用ExecutorService對象的submit()方法來提交Runnable實例或Callable實例。

④當不想提交任何任務時,調用ExecutorService對象的shutdown()方法來關閉線程池。

代碼示例:

public class ThreadPoolTest {
    public static void main(String[] args) throws Exception {
        // 建立足夠的線程來支持4個CPU並行的線程池
        // 建立一個具備固定線程數(6)的線程池
        ExecutorService pool = Executors.newFixedThreadPool(6);
        // 使用Lambda表達式建立Runnable對象
        Runnable target = () -> {
            for (int i = 0; i < 100 ; i++ )
            {
                System.out.println(Thread.currentThread().getName() + "的i值爲:" + i);
            }
        };
        // 向線程池中提交兩個線程
        pool.submit(target);
        pool.submit(target);
        // 關閉線程池
        pool.shutdown();
    }
}
複製代碼

  

Java8加強的ForkJoinPool

Java7提供了ForkJoinPool來支持將一個任務拆分紅多個「小任務」並行計算,再把多個「小任務」的結果合併成總的計算結果。ForkJoinPoolExecutorService的實現類,所以是一種特殊的線程池。

ForkJoinPool提供了以下兩個經常使用的構造器。

  • ForkJoinPool(int parallelism):建立一個包含parallelism個並行線程的ForkJoinPool

  • ForkJoinPool():以Runtime.availableProcessors()方法的返回值做爲parallelism參數來建立ForkJoinPool

Java8進一步擴展了ForkJoinPool的功能,Java8爲ForkJoinPool增長了通用池功能。

ForkJoinPool經過以下兩個靜態方法提供通用池功能。

  • ForkJoinPool commonPool():該方法返回一個通用池。

通用池的運行狀態不會受shutdown()shutdownNow()方法的影響。固然,若是程序直接執行System.exit(0);來終止虛擬機,通用池以及通用池中正在執行的任務都會被自動終止。

  • int getCommonPoolParallelism():該方法返回通用池的並行級別。

建立了ForkJoinPool實例以後,就可調用ForkJoinPoolsubmit(ForkJoin Task task)invoke(ForkJoinTask task)方法來執行指定任務了。

其中ForkJoinTask表明一個能夠並行、合併的任務。

ForkJoinTask是一個抽象類,它還有兩個抽象子類:RecursiveActionRecursive Task

其中Recursive Task表明有返回值的任務,而RecursiveAction表明沒有返回值的任務。

下面以執行沒有返回值的「大任務」(簡單地打印0-300的數值)爲例,程序將一個「大任務」拆分紅多個「小任務」,並將任務交給ForkJoinPool來執行。

// 繼承RecursiveAction來實現"可分解"的任務
class PrintTask extends RecursiveAction {
    // 每一個「小任務」只最多隻打印50個數
    private static final int THRESHOLD = 50;
    private int start;
    private int end;
    // 打印從start到end的任務
    public PrintTask(int start, int end) {
        this.start = start;
        this.end = end;
    }
    @Override
    protected void compute() {
        // 當end與start之間的差小於THRESHOLD時,開始打印
        if(end - start < THRESHOLD)
        {
            for (int i = start ; i < end ; i++ )
            {
                System.out.println(Thread.currentThread().getName() + "的i值:" + i);
            }
        }
        else
        {
            // 若是當end與start之間的差大於THRESHOLD時,即要打印的數超過50個
            // 將大任務分解成兩個小任務。
            int middle = (start + end) / 2;
            PrintTask left = new PrintTask(start, middle);
            PrintTask right = new PrintTask(middle, end);
            // 並行執行兩個「小任務」
            left.fork();
            right.fork();
        }
    }
}

/** * description: 主函數 **/
public class ForkJoinPoolTest {
    public static void main(String[] args) throws Exception {
        ForkJoinPool pool = new ForkJoinPool();
        // 提交可分解的PrintTask任務
        pool.submit(new PrintTask(0 , 300));
        pool.awaitTermination(2, TimeUnit.SECONDS);
        // 關閉線程池
        pool.shutdown();
    }
}
複製代碼

上面定義的任務是一個沒有返回值的打印任務,若是大任務是有返回值的任務,則可讓任務繼承Recursive Task<T>,其中泛型參數T就表明了該任務的返回值類型。下面程序示範了使用Recursive Task對一個長度爲100的數組的元素值進行累加。

// 繼承RecursiveTask來實現"可分解"的任務
class CalTask extends RecursiveTask<Integer> {
    // 每一個「小任務」只最多隻累加20個數
    private static final int THRESHOLD = 20;
    private int arr[];
    private int start;
    private int end;
    // 累加從start到end的數組元素
    public CalTask(int[] arr , int start, int end) {
        this.arr = arr;
        this.start = start;
        this.end = end;
    }
    @Override
    protected Integer compute() {
        int sum = 0;
        // 當end與start之間的差小於THRESHOLD時,開始進行實際累加
        if(end - start < THRESHOLD)
        {
            for (int i = start ; i < end ; i++ )
            {
                sum += arr[i];
            }
            return sum;
        }
        else
        {
            // 若是當end與start之間的差大於THRESHOLD時,即要累加的數超過20個時
            // 將大任務分解成兩個小任務。
            int middle = (start + end) / 2;
            CalTask left = new CalTask(arr , start, middle);
            CalTask right = new CalTask(arr , middle, end);
            // 並行執行兩個「小任務」
            left.fork();
            right.fork();
            // 把兩個「小任務」累加的結果合併起來
            return left.join() + right.join();    // ①
        }
    }
}


/** * description: 主函數 **/
public class Sum {
    public static void main(String[] args) throws Exception {
        int[] arr = new int[100];
        Random rand = new Random();
        int total = 0;
        // 初始化100個數字元素
        for (int i = 0 , len = arr.length; i < len ; i++ )
        {
            int tmp = rand.nextInt(20);
            // 對數組元素賦值,並將數組元素的值添加到sum總和中。
            total += (arr[i] = tmp);
        }
        System.out.println(total);
        // 建立一個通用池
        ForkJoinPool pool = ForkJoinPool.commonPool();
        // 提交可分解的CalTask任務
        Future<Integer> future = pool.submit(new CalTask(arr , 0 , arr.length));
        System.out.println(future.get());
        // 關閉線程池
        pool.shutdown();
    }
}
複製代碼

  

線程相關的類

ThreadLocal類

ThreadLocal,是Thread Local Variable(線程局部變量)的意思,它就是爲每個使用該變量的線程都提供一個變量值的副本,使每個線程均可以獨立地改變本身的副本,而不會和其餘線程的副本衝突。從線程的角度看,就好像每個線程都徹底擁有該變量同樣。

它只提供了以下三個public方法。

  • T get():返回此線程局部變量中當前線程副本中的值。

  • void remove():刪除此線程局部變量中當前線程的值。

  • void set(T value):設置此線程局部變量中當前線程副本中的值。

代碼示例:

/** * description: 帳戶類 **/
class Account {
    /* 定義一個ThreadLocal類型的變量,該變量將是一個線程局部變量 每一個線程都會保留該變量的一個副本 */
    private ThreadLocal<String> name = new ThreadLocal<>();
    // 定義一個初始化name成員變量的構造器
    public Account(String str) {
        this.name.set(str);
        // 下面代碼用於訪問當前線程的name副本的值
        System.out.println("---" + this.name.get());
    }
    // name的setter和getter方法
    public String getName() {
        return name.get();
    }
    public void setName(String str) {
        this.name.set(str);
    }
}


/** * description: 線程類 **/
class MyTest extends Thread {
    // 定義一個Account類型的成員變量
    private Account account;
    public MyTest(Account account, String name) {
        super(name);
        this.account = account;
    }
    public void run() {
        // 循環10次
        for (int i = 0 ; i < 10 ; i++)
        {
            // 當i == 6時輸出將帳戶名替換成當前線程名
            if (i == 6)
            {
                account.setName(getName());
            }
            // 輸出同一個帳戶的帳戶名和循環變量
            System.out.println(account.getName() + " 帳戶的i值:" + i);
        }
    }
}



/** * description: 主程序 **/
public class ThreadLocalTest {
    public static void main(String[] args) {
        // 啓動兩條線程,兩條線程共享同一個Account
        Account at = new Account("初始名");
        /* 雖然兩條線程共享同一個帳戶,即只有一個帳戶名 但因爲帳戶名是ThreadLocal類型的,因此每條線程 都徹底擁有各自的帳戶名副本,因此從i == 6以後,將看到兩條 線程訪問同一個帳戶時看到不一樣的帳戶名。 */
        new MyTest(at , "線程甲").start();
        new MyTest(at , "線程乙").start ();
    }
}
複製代碼

  
程序結果如圖:


線程局部變量互不干擾的情形

分析:

上面Account類中的三行粗體字代碼分別完成了建立ThreadLocal對象、從ThreadLocal中取出線程局部變量、修改線程局部變量的操做。

因爲程序中的帳戶名是一個ThreadLocal變量,因此雖然程序中只有一個Account對象,但兩個子線程將會產生兩個帳戶名(主線程也持有一個帳戶名的副本)。

兩個線程進行循環時都會在i=6時將帳戶名改成與線程名相同,這樣就能夠看到兩個線程擁有兩個帳戶名的情形,如圖所示。

從上面程序能夠看出,實際上帳戶名有三個副本,主線程一個,另外啓動的兩個線程各一個,它們的值互不干擾,每一個線程徹底擁有本身的ThreadLocal變量,這就是ThreadLocal的用途。

ThreadLocal和其餘全部的同步機制同樣,都是爲了解決多線程中對同一變量的訪問衝突。

在普通的同步機制中,是經過對象加鎖來實現多個線程對同一變量的安全訪問的。該變量是多個線程共享的,因此要使用這種同步機制,須要很細緻地分析在何時對變量進行讀寫,何時須要鎖定某個對象,何時釋放該對象的鎖等。在這種狀況下,系統並無將這份資源複製多份,只是採用了安全機制來控制對這份資源的訪問而已。

ThreadLocal從另外一個角度來解決多線程的併發訪問,ThreadLocal將須要併發訪問的資源複製多份,每一個線程擁有一份資源,每一個線程都擁有本身的資源副本,從而也就沒有必要對該變量進行同步了。

ThreadLocal提供了線程安全的共享對象,在編寫多線程代碼時,能夠把不安全的整個變量封裝進ThreadLocal,或者把該對象與線程相關的狀態使用ThreadLocal保存。

ThreadLocal並不能替代同步機制,二者面向的問題領域不一樣。同步機制是爲了同步多個線程對相同資源的併發訪問,是多個線程之間進行通訊的有效方式;

ThreadLocal是爲了隔離多個線程的數據共享,從根本上避免多個線程之間對共享資源(變量)的競爭,也就不須要對多個線程進行同步了。

一般建議:
若是多個線程之間須要共享資源,以達到線程之間的通訊功能,就使用同步機制;若是僅僅須要隔離多個線程之間的共享衝突,則可使用ThreadLocal

  

包裝線程不安全的集合

ArrayListLinkedListHashSetTreeSetHashMapTreeMap等都是線程不安全的,也就是說,當多個併發線程向這些集合中存、取元素時,就可能會破壞這些集合的數據完整性。

若是程序中有多個線程可能訪問以上這些集合,就可使用Collections提供的類方法把這些集合包裝成線程安全的集合。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對象,則能夠採用以下代碼:

//使用Collections的synchronizedMap方法將一個普通的HashMap包裝成線程安全的類
HashMap m=Collections.synchronizedMap(new HashMap());
複製代碼

  
tips:
若是須要把某個集合包裝成線程安全的集合,則應該在建立以後當即包裝,如上程序所示,當HashMap對象建立後當即被包裝成線程安全的HashMap對象。

線程安全的集合類

線程安全的集合類可分爲以下兩類:

  • Concurrent開頭的集合類,如ConcurrentHashMapConcurrentSkipListMapConcurrentSkip ListSet
    ConcurrentLinkedQueueConcurrentLinkedDeque

  • CopyOnWrite開頭的集合類,如CopyOnWriteArrayListCopyOnWriteArraySet

其中以Concurrent開頭的集合類表明了支持併發訪問的集合,它們能夠支持多個線程併發寫入訪問,這些寫入線程的全部操做都是線程安全的,但讀取操做沒必要鎖定。

Concurrent開頭的集合類採用了更復雜的算法來保證永遠不會鎖住整個集合,所以在併發寫入時有較好的性能。

在默認狀況下,ConcurrentHashMap支持16個線程併發寫入,當有超過16個線程併發向該Map中寫入數據時,可能有一些線程須要等待。實際上,程序經過設置concurrencyLevel構造參數(默認值爲16)來支持更多的併發寫入線程。

與前面介紹的HashMap和普通集合不一樣的是,由於ConcurrentLinkedQueueConcurrentHashMap支持多線程併發訪問,因此當使用迭代器來遍歷集合元素時,該迭代器可能不能反映出建立迭代器以後所作的修改,但程序不會拋出任何異常。

Java8擴展了ConcurrentHashMap的功能,Java8爲該類新增了30多個新方法,這些方法可藉助於StreamLambda表達式支持執行彙集操做。ConcurrentHashMap新增的方法大體可分爲以下三類:

  • forEach系列 (forEach,forEachKey,forEach Value,forEachEntry)

  • search系列 (search,searchKeys,search Values,searchEntries)

  • reduce系列 (reduce,reduce ToDouble,reduce ToLong,reduceKeys,reduceValues)

除此以外,ConcurrentHashMap還新增了mappingCount()newKeySet()等方法,加強後的ConcurrentHashMap更適合做爲緩存實現類使用。

  
CopyOnWriteAtraySet

因爲CopyOnWriteAtraySet的底層封裝了CopyOnWriteArmayList,所以它的實現機制徹底相似於CopyOnWriteArrayList集合。

對於CopyOnWriteArrayList集合,,它採用複製底層數組的方式來實現寫操做。

當線程對CopyOnWriteArrayList集合執行讀取操做時,線程將會直接讀取集合自己,無須加鎖與阻塞。

當線程對CopyOnWriteArrayList集合執行寫入操做時(包括調用add()、remove()set()`等方法)該集合會在底層複製一份新的數組,接下來對新的數組執行寫入操做。

因爲對 CopyOnWriteArmayList集合的寫入操做都是對數組的副本執行操做,所以它是線程安全的。

須要指出的是,因爲CopyOnWriteArrayList執行寫入操做時須要頻繁地複製數組,性能比較差。

但因爲讀操做與寫操做不是操做同一個數組,並且讀操做也不須要加鎖,所以讀操做就很快、很安全。因而可知,CopyOnWriteArayList適合用在讀取操做遠遠大於寫入操做的場景中,例如緩存等。


這些年看過的書:

《Effective Java》、《現代操做系統》、《TCP/IP詳解:卷一》、《代碼整潔之道》、《重構》、《Java程序性能優化》、《Spring實戰》、《Zookeeper》、《高性能MySQL》、《億級網站架構核心技術》、《可伸縮服務架構》、《Java編程思想》

說實話這些書不少只看了一部分,我一般會帶着問題看書,否則看着看着就睡着了,簡直是催眠良藥。

**最後,附一張面試前準備資料

2019年螞蟻金服、頭條、拼多多的面試總結(乾貨獻上)

有須要的夥伴私信我加入咱們羣聊809389099便可免費領取哦

相關文章
相關標籤/搜索