詳解多執行緒

java架構codi發表於2019-04-24

一個任務通常就是一個程式,每個執行中的程式就是一個程式。當一個程式執行時,內部可能包含了多個順序執行流,每個順序執行流就是一個執行緒。
  

程式

定義:

  當一個程式進入記憶體執行時,即變成一個程式。程式是處於執行過程中的程式,並且具有一定的獨立功能,程式是系統進行資源分配和排程的一個獨立單位。

程式的特點:

  1. 獨立性:是系統獨立存在的實體,擁有自己獨立的資源,有自己私有的地址空間。在沒有經過程式本身允許的情況下,一個使用者的程式不可以直接訪問其他程式的地址空間。

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

  3. 併發性:多個程式可以在單個處理器上併發執行,多個程式之間不會相互影響。

  

並行性和併發性

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

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

  

執行緒

  多執行緒擴充套件了多程式的概念,使得同一個程式可以同時併發處理多個任務。
  執行緒(Thread)也被成為輕量級的程式,執行緒是程式執行的單元,執行緒在程式中是獨立的、併發的執行流

  當程式被初始化後,主執行緒就被建立了。絕大數應用程式只需要有一個主執行緒,但也可以在程式內建立多條的執行緒,每個執行緒也是相互獨立的。

  一個程式可以擁有多個執行緒,一個執行緒必須有一個父程式。

  執行緒可以擁有自己的堆疊、自己的程式計數器和自己的區域性變數,但不擁有系統資源,它與父程式的其他執行緒共享該程式所擁有的全部資源,因此程式設計更加方便。

  執行緒是獨立執行的,它並不知道程式中是否還有其他的執行緒存在。執行緒的執行是搶佔式的,即:當前執行的執行緒在任何時候都有可能被掛起,以便另外一個執行緒可以執行。

  一個執行緒可以建立和撤銷另一個執行緒,同一個程式中多個執行緒之間可以併發執行。

  執行緒的排程和管理由程式本身負責完成。

  歸納而言:作業系統可以同時執行多個任務,每個任務就是程式;程式可以同時執行多個任務,每個任務就是執行緒

  

多執行緒的優點:

  1. 程式之間不能共享記憶體,但執行緒之間共享記憶體非常容易

  2. 系統建立程式要為該程式重新分配系統資源,但建立執行緒的代價則小得多。因此多執行緒實現多工併發比多執行緒的效率高。

  3. Java語言內建了多執行緒功能支撐,簡化了多執行緒的程式設計。

  
  

執行緒的建立和啟動

一、繼承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類建立執行緒類,多個執行緒之間無法共享執行緒類的例項變數

  

二、實現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介面建立執行緒類,多個執行緒可以共享執行緒類的例項變數

  

三、使用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需要在多條執行緒之間切換,於是執行緒狀態也會多次在執行、阻塞之間切換。

1、新建和就緒狀態

當程式使用new關鍵字建立了一個執行緒之後,該執行緒就處於新建狀態,此時它僅僅由Java虛擬機器為其分配記憶體,並且初始化其成員變數的值。此時的執行緒物件沒有表現出任何執行緒隊動態特徵,程式也不會執行執行緒的執行緒執行體。

當執行緒物件呼叫了start()方法之後,該執行緒處於就緒狀態,Java虛擬機器會為其建立方法呼叫棧和程式計數器,處於這個狀態中的執行緒並沒有開始執行,只是表示該執行緒可以執行了,至於該執行緒何時開始執行,取決於JVM裡執行緒排程器的排程。

tips:

  • 啟動執行緒使用start()方法,而不是run()方法,如果呼叫run()方法,則run()方法立即就會被執行,而且在run()方法返回之前,其他執行緒無法併發執行,也就是說,如果直接呼叫執行緒物件的run()方法,系統把執行緒物件當成一個普通物件,而run()方法也是一個普通方法,而不是執行緒執行體。

  • 如果直接呼叫執行緒物件的run()方法,則run()方法裡不能直接通過getName()方法來獲得當前執行執行緒的名字,而是需要使用Thread.currentThread()方法先獲得當前執行緒,再呼叫執行緒物件的getName()方法來獲得執行緒的名字。啟動執行緒的正確方法是呼叫Thread物件的start()方法,而不是直接呼叫run()方法,否則就變成單執行緒程式了。

  • 呼叫了執行緒的run()方法之後,該執行緒已經不再處於新建狀態,不要再次呼叫執行緒物件的start()方法。

2、執行和阻塞狀態

如果處於就緒狀態的執行緒獲得了CPU,開始執行run()方法的執行緒執行體,則該執行緒處於執行狀態。

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

當發生如下情況時,執行緒將會進入阻塞狀態:

1、執行緒呼叫sleep()方法主動放棄所佔用的處理器資源。

2、執行緒呼叫了一個阻塞式IO方法,在該方法返回之前,該執行緒被阻塞。

3、執行緒試圖獲得一個同步監視器,但該同步監視器正被其他執行緒所持有。

4、執行緒在等待某個通知(notify)。

5、程式呼叫了執行緒的suspend()方法將該執行緒掛起。但這個方法容易導致死鎖,所以應該儘量避免使用該方法。

針對上面幾種情況,當發生如下特定的情況時可以解除上面的阻塞,讓該執行緒重新進入就緒狀態。

1、呼叫sleep()方法的執行緒經過了指定時間。

2、執行緒呼叫的阻塞式IO方法已經返回。

3、 執行緒成功地獲得了試圖取得的同步監視器。

4、 執行緒正在等待某個通知時,其他執行緒發出了一個通知。

5、處於掛起狀態的執行緒被呼叫了resume()恢復方法。

詳解多執行緒
執行緒狀態轉化圖

從圖中可以看出,執行緒從阻塞狀態只能進入就緒狀態,無法直接進入執行狀態。

而就緒和執行狀態之間的轉換通常不受程式控制,而是由系統執行緒排程所決定。

當處於就緒狀態的執行緒獲得處理器資源時,該執行緒進入執行狀態;當處於執行狀態的執行緒失去處理器資源時,該執行緒進入就緒狀態。

但有一個方法例外,呼叫yield()方法可以讓執行狀態的執行緒轉入就緒狀態。

執行緒死亡

執行緒會以如下三種方式結束,結束後就處於死亡狀態。

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

  • 執行緒丟擲一個未捕獲的ExceptionError

  • 直接呼叫該執行緒的stop()方法來結束該執行緒——該方法容易導致死鎖,通常不推薦使用。

tips:

1、當主執行緒結束時,其他執行緒不受任何影響,並不會隨之結束。一旦子執行緒啟動起來後,它就擁有和主執行緒相同的地位,它不會受主執行緒的影響。

2、為了測試某個執行緒是否已經死亡,可以呼叫執行緒物件的isAlive()方法,當執行緒處於就緒、執行、阻塞三種狀態時,該方法將返回true;當執行緒處於新建、死亡兩種狀態時,該方法將返回false

3、不要試圖對一個已經死亡的執行緒呼叫start()方法使它重新啟動,死亡就是死亡,該執行緒將不可再次作為執行緒執行。線上程已經死亡的情況下再次呼叫start()方法將會引發IIIegalThreadException異常。

4、不能對死亡的執行緒呼叫start()方法,程式只能對新建狀態的執行緒呼叫start()方法,對新建的執行緒兩次呼叫start()方法也是錯誤的,會引發IIIegalThreadStateException異常。

  

控制執行緒

1、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);
        }
    }
}
複製程式碼

2、後臺執行緒

有一種執行緒,它是在後臺執行的,它的任務是為其他的執行緒提供服務,這種執行緒被稱為“後臺執行緒(Daemon Thread)”,又稱為“守護執行緒”或“精靈執行緒”。JVM的垃圾回收執行緒就是典型的後臺執行緒。

後臺執行緒有個特徵:如果所有的前臺執行緒都死亡,後臺執行緒會自動死亡。

呼叫Thread物件的setDaemon(true)方法可將指定執行緒設定成後臺執行緒。

tips:
1、Thread類還提供了一個isDaemon()方法,用於判斷指定執行緒是否為後臺執行緒。

2、前臺執行緒建立的子執行緒預設是前臺執行緒,後臺執行緒子執行緒預設是後臺執行緒。

3、前臺執行緒死亡後,JVM會通知後臺執行緒死亡,但從它接收指令到做出響應,需要一定時間。

而且要將某個執行緒設定為後臺執行緒,必須在該執行緒啟動之前設定,也就是說,setDaemon(true)必須在start()方法之前呼叫,否則會引發llegalThreadStateException異常。

3、執行緒睡眠:sleep

如果需要讓當前正在執行的執行緒暫停一段時間,並進入阻塞狀態,則可以通過呼叫Thread類的靜態sleep()方法來實現。

4、執行緒讓步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()方法來控制併發執行緒的執行。

5、改變執行緒優先順序

通過Thread類提供的setPriority(int newPriority)getPriority()方法來設定和返回指定執行緒的優先順序。

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

MAXPRIORITY:其值是10。

MIN PRIORITY:其值是1。

NORM_PRIORITY:其值是5。

  

執行緒同步

為了解決多個執行緒訪問同一個資料時,會出現問題,因此需要進行執行緒同步。就像前面介紹的檔案併發訪問,當有兩個程式併發修改同一個檔案時就有可能造成異常。

1、同步程式碼塊

為了解決執行緒同步問題,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() + "取錢失敗!餘額不足!");
            }
        }
        // 同步程式碼塊結束,該執行緒釋放同步鎖
    }
}
複製程式碼

  

2、同步方法

同步方法就是使用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()方法執行取錢操作,這樣也可以保證多個執行緒併發取錢的執行緒安全。

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

程式無法顯式釋放對同步監視器的鎖定,執行緒會在如下情況下釋放對同步監視器的鎖定。

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

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

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

  • 當前執行緒執行同步程式碼塊或同步方法時,程式執行了同步監視器物件的wait0方法,則當前執行緒暫停,並釋放同步監視器。

  
在如下所示的情況下,執行緒不會釋放同步監視器:

  • 執行緒執行同步程式碼塊或同步方法時,程式呼叫Thread.sleep()Thread.yield()方法來暫停當前執行緒的執行,當前執行緒不會釋放同步監視器。

  • 執行緒執行同步程式碼塊時,其他執行緒呼叫了該執行緒的suspend()方法將該執行緒掛起,該執行緒不會釋放同步監視器。當然,程式應該儘量避免使用suspend()resume()方法來控制執行緒。

4、同步鎖(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();
    }
}
複製程式碼

  

執行緒通訊

1、傳統的執行緒通訊——通過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即可免費領取哦


相關文章