Java多執行緒程式設計基礎知識彙總

Java伴我餘生發表於2020-07-11

多執行緒簡介

多工

  現代作業系統(Windows、Linux、MacOS)都可以執行多工,多工就是同時執行多個任務。例如在我們的計算機上,一般都同時跑著多個程式,例如瀏覽器,視訊播放器,音樂播放器,Word辦公軟體等等,由於CPU執行程式碼都是一條一條順序執行的,即時是單核CPU也可以同時執行多個任務,作業系統執行多個任務實際上就是輪流讓多個任務交替執行。即使是多核CPU,因為通常任務的數量是遠多於CPU的核數,所以任務也是交替執行的。

程式(Process)

  在計算機中,我們把一個任務稱為一個程式。瀏覽器就是一個程式,視訊播放器是另外一個程式,音樂播放器也是一個程式,在某些程式內部還需要同時執行多個子任務。例如我們在使用Word時,在打字的同時需要進行拼寫檢查,還可以在後臺進行列印,我們把子任務稱為執行緒。
程式和執行緒的關係:

  • 一個程式可以包含一個或多個執行緒(至少包含一個執行緒)
  • 執行緒是作業系統排程的最小任務單位
  • 如何排程執行緒完全由作業系統決定(程式自己不能決定執行緒何時執行,執行多長時間)

實現多工的三種方法:

  • 多程式模式(每個程式只有一個執行緒)
  • 多執行緒模式(一個程式有多個執行緒)
  • 多程式+多執行緒(複雜度最高)

多程式和多執行緒的對比:

  • 建立程式比建立執行緒開銷大(尤其是在Windows系統上)
  • 程式間通訊比執行緒間通訊慢
  • 多程式穩定性比多執行緒高(因為在多程式的情況下,一個程式的崩潰不會影響其他程式,而在多執行緒的情況下,任何一個執行緒的崩潰,會直接導致整個程式崩潰)

  Java語言內建多執行緒支援,一個Java程式實際上是一個JVM程式,JVM程式用一個主執行緒來執行main()方法,在main()方法中又可以啟動多個執行緒,此外,JVM還有負責垃圾回收的其他工作執行緒等。和單執行緒相比,多執行緒程式設計的特點在於:多執行緒經常需要讀寫共享資料,並且需要同步。例如,播放電影時,就必須由一個執行緒播放視訊,另一個執行緒播放音訊,兩個執行緒需要協調執行,否則畫面和聲音就不同步。因此,多執行緒程式設計的複雜度高,除錯更困難。

Java多執行緒基礎

  建立一個執行緒物件,並啟動一個新的執行緒。建立執行緒物件的方法有兩種:第一種就是建立MyThread類,去繼承Thread類,覆寫run()方法,建立MyThread例項,呼叫start()啟動執行緒。第二種:如果一個類已經從某個類派生,無法從Thread繼承,就可以通過實現Runnable介面,重寫run()方法,在main()方法中建立Runnable例項,建立Thread例項並傳入Runnable,呼叫start()啟動執行緒。注意:必須呼叫Thread例項的start()方法才能啟動新執行緒,如果我們檢視Thread類的原始碼,會看到start()方法內部呼叫了一個private native void start0()方法,native修飾符表示這個方法是由JVM虛擬機器內部的C程式碼實現的,不是由Java程式碼實現的。
總結:Java用Thread物件表示一個執行緒,通過呼叫start()啟動一個新執行緒,一個執行緒物件只能呼叫一次start()方法;執行緒的執行程式碼寫在run()方法中,一旦run()方法執行完畢,執行緒就結束了;執行緒排程由作業系統決定,程式本身無法決定排程順序。
執行緒的狀態:

  • New:新建立的執行緒,尚未執行;
  • Runnable:執行中的執行緒,正在執行run()方法的Java程式碼;
  • Blocked:執行中的執行緒,因為某些操作被阻塞而掛起;
  • Waiting:執行中的執行緒,因為某些操作在等待中;
  • Timed Waiting:執行中的執行緒,因為執行sleep()方法正在計時等待;
  • Terminated:執行緒已終止,因為run()方法執行完畢。

執行緒終止的原因:

  • run()方法執行到return語句返回(執行緒正常終止)
  • run()方法因為未捕獲的異常導致執行緒終止(執行緒意外終止)
  • 對某個執行緒的Thread例項呼叫stop()方法強制終止(強烈不推薦使用)

  通過對另一個執行緒物件呼叫join()方法可以等待其執行結束,可以指定等待時間,超過等待時間執行緒仍然沒有結束就不再等待;對已經執行結束的執行緒呼叫join()方法會立刻返回。

  中斷執行緒:如果執行緒需要執行一個長時間任務,就可能需要能中斷執行緒。中斷執行緒就是其他執行緒給該執行緒傳送一個資訊,該執行緒收到訊號後,結束執行run()方法。例如我們從網路下載一個100M的檔案,如果網速很慢,我們等得不耐煩,就可能在下載過程中點“取消”,這時,程式就需要中斷下載執行緒的執行。中斷執行緒需要通過檢測isInterrupted標誌,其他執行緒需要通過呼叫interrupt()方法中斷該執行緒。如果執行緒處於等待狀態,該執行緒會捕獲InterruptedException。捕獲到InterruptedException說明有其他對其執行緒呼叫了interrupt()方法,通常情況下該執行緒應該立刻結束執行。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        t.start();
        Thread.sleep(1000);
        t.interrupt(); // 中斷t執行緒
        t.join(); // 等待t執行緒結束
        System.out.println("end");
    }
}

class MyThread extends Thread {
    public void run() {
        Thread hello = new HelloThread();
        hello.start(); // 啟動hello執行緒
        try {
            hello.join(); // 等待hello執行緒結束
        } catch (InterruptedException e) {
            System.out.println("interrupted!");
        }
        hello.interrupt();
    }
}

class HelloThread extends Thread {
    public void run() {
        int n = 0;
        while (!isInterrupted()) {
            n++;
            System.out.println(n + " hello!");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                break;
            }
        }
    }
}

  還可以通過設定running標誌位,執行緒間共享變數需要使用volatile關鍵字標記,確保執行緒能讀取到更新後的變數值。為什麼要對執行緒間共享的變數用關鍵字volatile宣告?這涉及到Java的記憶體模型。在Java虛擬機器中,變數的值儲存在主記憶體中,但是,當執行緒訪問變數時,它會先獲取一個副本,並儲存在自己的工作記憶體中。如果執行緒修改了變數的值,虛擬機器會在某個時刻把修改後的值回寫到主記憶體,但是,這個時間是不確定的!這會導致如果一個執行緒更新了某個變數,另一個執行緒讀取的值可能還是更新前的。

volatile關鍵字解決的是可見性問題:當一個執行緒修改了某個共享變數的值,其他執行緒能夠立刻看到修改後的值。因此,volatile關鍵字的目的是告訴虛擬機器:每次訪問變數時,總是獲取主記憶體的最新值,每次修改變數後,立刻回寫到主記憶體。

  守護執行緒:為其他執行緒服務的執行緒。在JVM中,所有非守護執行緒都執行完畢後,無論有沒有守護執行緒,虛擬機器都會自動退出。因此,JVM退出時,不必關心守護執行緒是否已結束。如何建立守護執行緒呢?方法和普通執行緒一樣,只是在呼叫start()方法前,呼叫setDaemon(true)把該執行緒標記為守護執行緒。

Thread t = new MyThread();
t.setDaemon(true);
t.start();

需要注意的是:守護執行緒不能持有任何需要關閉的資源,例如開啟檔案等,因為虛擬機器退出時,守護執行緒沒有任何機會來關閉檔案,這會導致資料丟失。

多執行緒同步

  當多個執行緒同時執行時,執行緒的排程由作業系統決定,程式本身無法決定。因此,任何一個執行緒都有可能在任何指令處被作業系統暫停,然後在某個時間段後繼續執行,如果多個執行緒同時讀寫共享變數,會出現資料不一致的問題。對共享變數進行寫入時,必須保證是原子操作,原子操作是指不能被中斷的一個或一系列操作。例如,對於語句:n = n + 1;看上去是一行語句,實際上對應了3條指令,因此為了保證一系列操作為原子操作,必須保證一系列操作在執行過程中不被其他執行緒執行。ava程式使用synchronized關鍵字對一個物件進行加鎖,synchronized保證了程式碼塊在任意時刻最多隻有一個執行緒能執行。由於synchronized程式碼塊無法併發執行,所以使用synchronized會導致效能下降。如何使用synchronized?首先找出修改共享變數的執行緒程式碼塊,選擇一個例項作為鎖,使用synchronized(lockObject) { ... }。在使用synchronized的時候,不必擔心丟擲異常。因為無論是否有異常,都會在synchronized結束處正確釋放鎖。

  多執行緒同時修改變數,會造成邏輯錯誤,需要通過synchronized同步,同步的本質就是給指定物件加鎖,注意加鎖物件必須是同一個例項,對於JVM定義的單個原子操作不需要同步。JVM規範定義了幾種原子操作:基本型別(long和double除外)賦值,例如:int n = m;引用型別賦值,例如:List list = anotherList。long和double是64位資料,JVM沒有明確規定64位賦值操作是不是一個原子操作,不過在x64平臺的JVM是把long和double的賦值作為原子操作實現的。

  同步方法:當程式執行synchronized程式碼塊時,首先要獲得synchronized指定的鎖。在我們新增synchronized塊時,我們需要先知道鎖住的哪個物件。讓執行緒自己選擇鎖物件往往會使得程式碼邏輯混亂,也不利於封裝。更好的方法是把synchronized邏輯封裝起來。資料封裝:把同步邏輯封裝到持有資料的例項中。當我們對this進行加鎖時,我們可以使用synchronized來修飾方法,這樣我們就可以把同步程式碼塊自動變成方法識別。下面的兩種寫法是等價的。

public synchronized void add(int n) {
      n += 1;
}

public void add(int n) {
      synchronized(this){
          n += 1;
      }
}

而靜態方法鎖住的是Class例項:如下

public class A {
      private static count;
      
      public static synchronized void add(int n){
            count += n;      
      }    
}

等價於下面這種寫法:

public class A {
      private static count;
      
      public static void add(int n){
            synchronized(A.class) {
                  count += n; 
            }      
      }    
}

Java中執行緒安全的類:

  • 不變類:例如String, Integer, LocalDate,因為這些類被final修飾,一但建立,例項成員變數就不能改變,不能寫只能讀
  • 沒有成員變數的類:例如Math,這些工具只提供了工具方法,自身沒有成員變數
  • 正確使用synchronized得類,例如StringBuffer
    其它的類都是非執行緒安全的類,不能在多執行緒中共享例項並修改,例如ArrayList,但是可以在多執行緒以只讀的方式共享

死鎖

  要執行 synchronized程式碼塊,必須首先獲得指定物件的鎖,Java中的執行緒鎖是可重入鎖。什麼叫可重入鎖?JVM允許同一個執行緒重複獲取同一個鎖,這種能被同一個執行緒反覆獲取的鎖,就叫做可重入鎖。Java的執行緒可以獲取多個不同物件的鎖。不同的執行緒在獲取多個不同物件的鎖的時候,可能會導致死鎖。例如兩個執行緒根被執行下面兩個方法,就會導致死鎖。

死鎖形成的條件:兩個執行緒各自持有不同的鎖,然後各自試圖獲取對方手裡的鎖,造成了雙方無限等待下去,導致死鎖。

死鎖發生後,沒有任何機制能解除死鎖,只能強制結束JVM程式。如何避免死鎖?執行緒獲取鎖的順序要一致。

wait/notify

  synchronized解決了多執行緒競爭的問題,但是synchronized並沒有解決多執行緒協調的問題。

wait和notify用於多執行緒協調執行:在synchronized內部可以呼叫wait()使執行緒進入等待狀態,必須在已獲得的鎖物件上呼叫wait()方法,在synchronized內部可以呼叫notify()或notifyAll()喚醒其他等待執行緒,必須在已獲得的鎖物件上呼叫notify()或notifyAll()方法,已喚醒的執行緒還需要重新獲得鎖後才能繼續執行。

JUC包

  從Java 5開始,引入了一個高階的處理併發的java.util.concurrent包,它提供了大量更高階的併發功能,能大大簡化多執行緒程式的編寫。執行緒同步是因為多執行緒讀寫競爭資源需要同步,Java語言提供了synchronized/wait/notify來實現多執行緒的同步,但是編寫多執行緒同步仍然很困難。jdk1.5提供的高階的java.util.concurrent包,提供了更高階的同步功能,可以簡化多執行緒的編寫,java.util.concurrent.locks包提供的ReentrantLock用於替代synchronized加鎖。因為synchronized是Java語言層面提供的語法,所以我們不需要考慮異常,而ReentrantLock是Java程式碼實現的鎖,我們就必須先獲取鎖,然後在finally中正確釋放鎖。

ReentrantLock也是可重入鎖,一個執行緒可多次獲取同一個鎖,lock()方法獲取鎖,和synchronized不同的是,ReentrantLock可以通過tryLock()方法嘗試獲取鎖並指定超時:

if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try {
        ...
    } finally {
        lock.unlock();
    }
}

  但是有些時候,這種保護有點過頭。因為我們發現,任何時刻,只允許一個執行緒修改,但是,對於某些方法只讀取資料,不修改資料,它實際上允許多個執行緒同時呼叫,實際上我們想要的是:允許多個執行緒同時讀,但只要有一個執行緒在寫,其他執行緒就必須等待。使用ReadWriteLock可以解決這個問題,它保證只允許一個執行緒寫入(其他執行緒既不能寫入也不能讀取),沒有寫入時,多個執行緒允許同時讀(提高效能)。

public class Counter {
    private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
    private final Lock rlock = rwlock.readLock();
    private final Lock wlock = rwlock.writeLock();
    private int[] counts = new int[10];

    public void inc(int index) {
        wlock.lock(); // 加寫鎖
        try {
            counts[index] += 1;
        } finally {
            wlock.unlock(); // 釋放寫鎖
        }
    }

    public int[] get() {
        rlock.lock(); // 加讀鎖
        try {
            return Arrays.copyOf(counts, counts.length);
        } finally {
            rlock.unlock(); // 釋放讀鎖
        }
    }
}

使用ReadWriteLock時,適用條件是同一個資料,有大量執行緒讀取,但僅有少數執行緒修改。

  使用ReentrantLock比直接使用synchronized更安全,可以替代synchronized進行執行緒同步。但是,synchronized可以配合wait和notify實現執行緒在條件不滿足時等待,條件滿足時喚醒,用ReentrantLock我們怎麼編寫wait和notify的功能呢?答案是使用Condition物件來實現wait和notify的功能。

class TaskQueue {
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private Queue<String> queue = new LinkedList<>();

    public void addTask(String s) {
        lock.lock();
        try {
            queue.add(s);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public String getTask() {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                condition.await();
            }
            return queue.remove();
        } finally {
            lock.unlock();
        }
    }
}

可見,使用Condition時,引用的Condition物件必須從Lock例項的newCondition()返回,這樣才能獲得一個繫結了Lock例項的Condition例項。Condition提供的await()、signal()、signalAll()原理和synchronized鎖物件的wait()、notify()、notifyAll()是一致的,並且其行為也是一樣的:await()會釋放當前鎖,進入等待狀態;signal()會喚醒某個等待執行緒;signalAll()會喚醒所有等待執行緒;喚醒執行緒從await()返回後需要重新獲得鎖。此外,和tryLock()類似,await()可以在等待指定時間後,如果還沒有被其他執行緒通過signal()或signalAll()喚醒,可以自己醒來。

  Concurrent集合:java.util.concurrent提供了執行緒安全的Blocking集合:ArrayBlockingQueue。BlockingQueue的意思就是說,當一個執行緒呼叫這個TaskQueue的getTask()方法時,該方法內部可能會讓執行緒變成等待狀態,直到佇列條件滿足不為空,執行緒被喚醒後,getTask()方法才會返回。

介面 執行緒不安全的實現類 執行緒安全的實現類
List ArrayList CopyOnWriteArrayList
Map HashMap ConcurrentHashMap
Set HashSet / TreeSet CopyOnWriteArraySet
Queue ArrayDeque / LinkedList ArrayBlockingQueue / LinkedBlockingQueue
Deque ArrayDeque / LinkedList LinkedBlockingDeque

  使用java.util.concurrent包提供的執行緒安全的併發集合可以大大簡化多執行緒程式設計,多執行緒同時讀寫併發集合是安全的,儘量使用Java標準庫提供的併發集合,避免自己編寫同步程式碼。

  Atomic:java.util.concurrent.atomic提供了一組原子型別操作,Atomic類是通過無鎖(lock-free)的方式實現的執行緒安全(thread-safe)訪問。它的主要原理是利用了CAS:Compare and Set。如果我們自己通過CAS編寫incrementAndGet(),大概如下:

public int incrementAndGet(AtomicInteger var) {
    int prev, next;
    do {
        prev = var.get();
        next = prev + 1;
    } while ( ! var.compareAndSet(prev, next));
    return next;
}

CAS是指,在這個操作中,如果AtomicInteger的當前值是prev,那麼就更新為next,返回true。如果AtomicInteger的當前值不是prev,就什麼也不幹,返回false。通過CAS操作並配合do ... while迴圈,即使其他執行緒修改了AtomicInteger的值,最終的結果也是正確的。用java.util.concurrent.atomic提供的原子操作可以簡化多執行緒程式設計,原子操作實現了無鎖的執行緒安全,適用於計數器,累加器等。

ExecutorService

  由於建立執行緒需要作業系統資源(執行緒資源、棧空間),頻繁建立和銷燬執行緒需要消耗大量時間。如果可以複用一組執行緒,那麼我們就可以把很多小任務讓一組執行緒來執行,而不是一個任務對應一個新執行緒。這種能接收大量小任務並進行分發處理的就是執行緒池。所以執行緒池內部維護了若干個執行緒,沒有任務的時候,這些執行緒都處於等待狀態。如果有新任務,就分配一個空閒執行緒執行。如果所有執行緒都處於忙碌狀態,新任務要麼放入佇列等待,要麼增加一個新執行緒進行處理。Java標準庫提供了ExecutorService介面表示執行緒池,常用用法如下:

// 建立固定大小的執行緒池:
ExecutorService executor = Executors.newFixedThreadPool(4);
// 提交任務:
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);

因為ExecutorService只是介面,Java標準庫提供的幾個常用實現類有:

  • FixedThreadPool:執行緒數固定的執行緒池;
  • CachedThreadPool:執行緒數根據任務動態調整的執行緒池;
  • SingleThreadExecutor:僅單執行緒執行的執行緒池(只包含一個執行緒,所有的任務只能以單執行緒的形式執行)

建立這些執行緒池的方法都被封裝到Executors這個類中。我們以FixedThreadPool為例,看看執行緒池的執行邏輯:

import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) {
        // 建立一個固定大小的執行緒池:
        ExecutorService es = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 6; i++) {
            es.submit(new Task("" + i));
        }
        // 關閉執行緒池:
        es.shutdown();
    }
}

class Task implements Runnable {
    private final String name;

    public Task(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println("start task " + name);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        System.out.println("end task " + name);
    }
}

  還有一種任務,需要定期反覆執行,例如,每秒重新整理證券價格。這種任務本身固定,需要反覆執行的,可以使用ScheduledThreadPool。放入ScheduledThreadPool的任務可以定期反覆執行。Java標準庫還提供了一個java.util.Timer類,這個類也可以定期執行任務,但是,一個Timer會對應一個Thread,所以,一個Timer只能定期執行一個任務,多個定時任務必須啟動多個Timer,而一個ScheduledThreadPool就可以排程多個定時任務,所以,我們完全可以用ScheduledThreadPool取代舊的Timer。
總結:

  • JDK提供了ExecutorService實現了執行緒池功能;
  • 執行緒池內部維護一組執行緒,可以高效執行大量小任務;
  • Executors提供了靜態方法建立不同型別的ExecutorService;
  • 必須呼叫shutdown()關閉ExecutorService;
  • ScheduledThreadPool可以定期排程多個任務。

  Runnable介面有個問題,它的方法沒有返回值。如果任務需要一個返回結果,那麼只能儲存到變數,還要提供額外的方法讀取,非常不便。所以,Java標準庫還提供了一個Callable介面,和Runnable介面比,它多了一個返回值.

class Task implements Callable<String> {
    public String call() throws Exception {
        return longTimeCalculation(); 
    }
}

並且Callable介面是一個泛型介面,可以返回指定型別的結果。現在的問題是,如何獲得非同步執行的結果?如果仔細看ExecutorService.submit()方法,可以看到,它返回了一個Future型別,一個Future型別的例項代表一個未來能獲取結果的物件.

ExecutorService executor = Executors.newFixedThreadPool(4); 
// 定義任務:
Callable<String> task = new Task();
// 提交任務並獲得Future:
Future<String> future = executor.submit(task);
// 從Future獲取非同步執行返回的結果:
String result = future.get(); // 可能阻塞

當我們提交一個Callable任務後,我們會同時獲得一個Future物件,然後,我們在主執行緒某個時刻呼叫Future物件的get()方法,就可以獲得非同步執行的結果。在呼叫get()時,如果非同步任務已經完成,我們就直接獲得結果。如果非同步任務還沒有完成,那麼get()會阻塞,直到任務完成後才返回結果。
一個Future介面表示一個未來可能會返回的結果,它定義的方法有:

  • get():獲取結果(可能會等待)
  • get(long timeout, TimeUnit unit):獲取結果,但只等待指定的時間;
  • cancel(boolean mayInterruptIfRunning):取消當前任務;
  • isDone():判斷任務是否已完成。

  CompletableFuture:從Java 8開始引入了CompletableFuture,它針對Future做了改進,可以傳入回撥物件,當非同步任務結束時,會自動回撥某個物件的方法,非同步任務出錯時,也會自動回撥某個物件的方法,所以當主執行緒設定好回撥後,不再關心非同步任務的執行。
CompletableFuture的基本用法:

CompletableFuture<String> cf = CompletableFuture.supplyAsync(非同步執行例項);
cf.thenAccept("獲得結果後的操作");
cf.exceptionnally("發生異常時的操作")

建立一個CompletableFuture是通過CompletableFuture.supplyAsync()實現的,它需要一個實現了Supplier介面的物件。可見CompletableFuture的優點是:非同步任務結束時,會自動回撥某個物件的方法;非同步任務出錯時,會自動回撥某個物件的方法;主執行緒設定好回撥後,不再關心非同步任務的執行。如果只是實現了非同步回撥機制,我們還看不出CompletableFuture相比Future的優勢。CompletableFuture更強大的功能是,多個CompletableFuture可以序列執行,例如,定義兩個CompletableFuture,第一個CompletableFuture根據證券名稱查詢證券程式碼,第二個CompletableFuture根據證券程式碼查詢證券價格,這兩個CompletableFuture實現序列操作如下:

public class Main {
    public static void main(String[] args) throws Exception {
        // 第一個任務:
        CompletableFuture<String> cfQuery = CompletableFuture.supplyAsync(() -> {
            return queryCode("中國石油");
        });
        // cfQuery成功後繼續執行下一個任務:
        CompletableFuture<Double> cfFetch = cfQuery.thenApplyAsync((code) -> {
            return fetchPrice(code);
        });
        // cfFetch成功後列印結果:
        cfFetch.thenAccept((result) -> {
            System.out.println("price: " + result);
        });
        // 主執行緒不要立刻結束,否則CompletableFuture預設使用的執行緒池會立刻關閉:
        Thread.sleep(2000);
    }

    static String queryCode(String name) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        return "601857";
    }

    static Double fetchPrice(String code) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        return 5 + Math.random() * 20;
    }
}

總結:CompletableFuture可以指定非同步處理流程:

  • thenAccept()處理正常結果;
  • exceptional()處理異常結果;
  • thenApplyAsync()用於序列化另一個CompletableFuture;
  • anyOf()和allOf()用於並行化多個CompletableFuture。

Fork Join

  Java 7開始引入了一種新的Fork/Join執行緒池,它可以執行一種特殊的任務:把一個大任務拆成多個小任務並行執行。Fork/Join執行緒池在Java標準庫中就有應用。Java標準庫提供的java.util.Arrays.parallelSort(array)可以進行並行排序,它的原理就是內部通過Fork/Join對大陣列分拆進行並行排序,在多核CPU上就可以大大提高排序的速度。Fork/Join是一種基於“分治”的演算法:通過分解任務,並行執行,最後合併結果得到最終結果。ForkJoinPool執行緒池可以把一個大任務分拆成小任務並行執行,任務類必須繼承自RecursiveTask或RecursiveAction。使用Fork/Join模式可以進行平行計算以提高效率。

ThreadLocal

  多執行緒是Java實現多工的基礎,Thread物件代表一個執行緒,我們可以在程式碼中呼叫Thread.currentThread()獲取當前執行緒。Java標準庫提供了一個特殊的ThreadLocal,它可以在一個執行緒中傳遞同一個物件,注意到普通的方法呼叫一定是同一個執行緒執行的。ThreadLocal例項通常總是以靜態欄位初始化如:static ThreadLocal threadLocalUser = new ThreadLocal<>();實際上,可以把ThreadLocal看成一個全域性Map<Thread, Object>:每個執行緒獲取ThreadLocal變數時,總是使用Thread自身作為key:因此,ThreadLocal相當於給每個執行緒都開闢了一個獨立的儲存空間,各個執行緒的ThreadLocal關聯的例項互不干擾。最後,特別注意ThreadLocal一定要在finally中清除:這是因為當前執行緒執行完相關程式碼後,很可能會被重新放入執行緒池中,如果ThreadLocal沒有被清除,該執行緒執行其他程式碼時,會把上一次的狀態帶進去。ThreadLocal適合在一個執行緒的處理流程中保持上下文(避免了同一引數在所有方法中傳遞)。

【說明】:本文參考了廖雪峰官方網站的Java教程,廖雪峰官方網站的多執行緒教程連結:https://www.liaoxuefeng.com/wiki/1252599548343744/1255943750561472

相關文章