適用於Android開發者的多執行緒總結

語言: CN / TW / HK

highlight: a11y-dark

前言

對於程式設計師來說,執行緒一直是我們開發中最常出現的技術,可以說,使用起來完全沒問題,通過百度以及熟悉度可以順手拈來,但是對於深入理解,卻不是所有人都能做到,寫這篇文章的目的,主要用於自己進行復習,總結,未來也會持續修改該文,目前,作者對多執行緒並不深入,因此文中摘抄了很多大佬的一些文章,感謝大佬們的開源。文中附有相關連結,可自行跳轉,感謝呦!!

👓 執行緒、程序

🚗 程序

指在系統中正在執行的一個應用程式;程式一旦執行就是程序;是系統進行資源分配的基本單位

🧷 執行緒

程序之內獨立執行的一個單元執行流。執行緒CPU排程和執行的最小單位,包含在程序之中,是程序中的實際運作單位。一條執行緒指的是程序中一個單一順序的控制流,一個程序中可以併發多個執行緒,每條執行緒並行執行不同的任務。

👓 為什麼使用多執行緒

  • 主執行緒不能執行耗時較長的任務,否則會阻塞UI執行緒,引起ANR、卡頓等問題(只能在UI執行緒操作UI檢視,不能在子執行緒中操作)
  • Android 強制要求開發者在發起網路請求時,必須在工作執行緒,不能在主執行緒,否則丟擲NetworkOnMainThreadException

👓 多執行緒場景

  • Android中,App從一啟動。就算是一個空白demo,它也是多執行緒應用,原因是因為App是執行在art上,art自帶GC執行緒,再加上App必有的主執行緒(UI執行緒),就組成了一個多執行緒應用
  • 日常使用的三方庫,比如Okhttp、Glide、RxJava
  • 處理耗時任務,刪除資料,清空快取,操作資料庫等等

    UI 執行緒為什麼不會結束?因為它在初始化完畢後會執⾏死迴圈,迴圈的內容是重新整理界⾯

👓 併發和並行

  • **併發和並行最開始都是作業系統中的概念,表示的是CPU執行多個任務的方式。這兩個概念極容易混淆。
  • 如果使用的是單核CPU,那麼我們是無法執行並行操作的,只能做到併發,這樣來充分呼叫CPU的資源。
  • 如果我們使用的是多核CPU,我們才可以真正的意義上做到並行操作。
  • 併發(Concurrent),在作業系統中,是指一個時間段中有幾個程式都處於已啟動執行到執行完畢之間,且這幾個程式都是在同一個處理機上執行。作業系統的時間片分時排程。打遊戲和聽音樂兩件事情在同一個時間段內都是在同一臺電腦上完成了從開始到結束的動作。那麼,就可以說聽音樂和打遊戲是併發的。

  • 並行(Parallel),當系統CPu有一個以上的核心時,當一個核心執行一個程序時,另一個核心可以執行另一個程序,兩個程序互不搶佔核心資源,可以同時進行,這種方式我們稱之為並行(Parallel)。

演示在同一個程式中,併發跟並行發生的場景 ``` js public class TestDemo {

  public static void main(String[] args) {
    //併發(搶佔共有資源)
    TestThread testThread = new TestThread();
    Thread thread = new Thread(testThread, "thread-01");
    Thread thread1 = new Thread(testThread, "thread-02");
    Thread thread2 = new Thread(testThread, "thread-03");
    Thread thread3 = new Thread(testThread, "thread-04");
    thread.start();
    thread1.start();
    thread2.start();
    thread3.start();

    //並行(互相不搶佔資源)
    TestThread testThread1 = new TestThread();
    TestThread testThread2 = new TestThread();
    testThread1.start();
    testThread2.start();
  }

  static class TestThread extends Thread {
    public TestThread() { super("TestThread"); }
    private int count = 10;

    @Override
    public void run() {
      super.run();
      System.out.println(count--);
    }
  }

} ```

👓 非同步和同步

同步和非同步關注的是訊息通訊機制.

  • 同步是指: 傳送方發出資料後, 等待接收方發回響應後才發下一個數據包的通訊方式. 就是在發出一個呼叫時, 在沒有得到結果之前, 該呼叫就不返回, 但是一旦呼叫返回, 就得到返回值了. 也就是由"呼叫者"主動等待這個"呼叫"的結果.

  • 非同步是指: 傳送方發出資料後, 不等待接收方發回響應, 接著傳送下個數據包的通訊方式. 當一個非同步過程呼叫發出後, 呼叫者不會立刻得到結果. 而是在呼叫發出後, "被呼叫者"通過狀態、通知來通知呼叫者, 或通過回撥函式處理這個呼叫.

👓 使用執行緒的幾種方式

👓 Java

🎯 Thread

```js Thread thread1 = new Thread() { @Override public void run() { super.run(); System.out.println("直接new出來,簡單粗暴"); } }; thread1.start();

```

🎯 Thread+Runnable

js Runnable target = new Runnable() { @Override public void run() { System.out.println("例項一個runnable物件,交由執行緒使用,方便複用"); } }; Thread thread = new Thread(target); Thread thread2 = new Thread(target); thread.start(); thread2.start();

👓 ThreadFactory+Runnable

```js ThreadFactory threadFactory = new ThreadFactory() { @Override public Thread newThread(Runnable runnable) { return new Thread(runnable, "Thread-" + new Random().nextInt(1000)); } };

Runnable target1 = new Runnable() {
  @Override
  public void run() {
    System.out.println("例項一個runnable物件,交由執行緒工廠使用,得到thread物件,方便複用");
  }
};

threadFactory.newThread(target1);

```

🎯 ExecutorService+Runnable

```js Executor executor = Executors.newSingleThreadExecutor(); Executor executor1 = Executors.newCachedThreadPool(); Executor executor2 = Executors.newFixedThreadPool(10); Executor executor3 = Executors.newScheduledThreadPool(1);

Runnable target2 = new Runnable() {
  @Override
  public void run() {
    System.out.println("例項一個runnable物件,交由執行緒池使用,執行緒池幫助集中管理執行緒,避免資源浪費,方便複用,");
  }
};
executor.execute(target2);
executor1.execute(target2);
executor2.execute(target2);
executor3.execute(target2);

```

Future+Callable

```js Callable callable = new Callable() { @Override public String call() { try { Thread.sleep(1500); } catch (InterruptedException e) { e.printStackTrace(); } return "提交一個任務到執行緒池裡面去!"; } };

ExecutorService executor4 = Executors.newCachedThreadPool();
Future<String> future = executor4.submit(callable);
try {
  String result = future.get();
  System.out.println("result: " + result);
} catch (InterruptedException | ExecutionException e)
{
  e.printStackTrace();
}

```

👓 android專屬

  • Handler
  • IntentService
  • HandlerThread
  • AsyncTask
  • Rxjava

    考慮到篇幅以及這些非同步機制都是大家比較熟知的,暫不列出使用方式,貼上大佬們寫過的文章連結,供大家瞭解,如果後續有需要在進行列出,不然篇幅太長,看著看著很容易噁心

👓 kotlin

👓 執行緒安全

## 👓 為什麼會出現執行緒安全問題 - Java 記憶體模型規定了所有的變數都儲存在主記憶體中,每條執行緒有自己的工作記憶體。 - 執行緒的工作記憶體中儲存了該執行緒中用到的變數的主記憶體副本拷貝,執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接讀寫主記憶體。 - 執行緒訪問一個變數,首先將變數從主記憶體拷貝到工作記憶體,對變數的寫操作,不會馬上同步到主記憶體。 - 不同的執行緒之間也無法直接訪問對方工作記憶體中的變數,執行緒間變數的傳遞均需要自己的工作記憶體和主存之間進行資料同步進行。

🎯 解決方式

解決方式部分摘抄自面試官:說說多執行緒併發問題\ 本人對於多執行緒相知不多,目前也在學習中,該部分暫時引用,等後續會加入自己的見解進行改寫 - 保證共享資源在同一時間只能由一個執行緒進行操作(原子性,有序性)。 - 將執行緒操作的結果及時重新整理,保證其他執行緒可以立即獲取到修改後的最新資料(可見性)。 ### 🎯 volatile - 保證可見性,不保證原子性 1. 當寫一個volatile變數時,JVM會把本地記憶體的變數強制重新整理到主記憶體中 2. 這個寫操作導致其他執行緒中的快取無效,其他執行緒讀,會從主記憶體讀。volatile的寫操作對其它執行緒實時可見。 - 禁止指令重排序 1. 不會對存在依賴關係的指令重排序,例如 a = 1;b = a; a 和b存在依賴關係,不會被重排序 2. 不能影響單執行緒下的執行結果。比如:a=1;b=2;c=a+b這三個操作,前兩個操作可以重排序,但是c=a+b不會被重排序,因為要保證結果是3

🎯 使用場景

對於一個變數,只有一個執行緒執行寫操作,其它執行緒都是讀操作,這時候可以用 volatile 修飾這個變數。

🎯 單例雙重鎖為什麼要用到volatile?

```js public class TestInstance {

private static volatile TestInstance mInstance;

public static TestInstance getInstance(){ //1 if (mInstance == null){ //2 synchronized (TestInstance.class){ //3 if (mInstance == null){ //4 mInstance = new TestInstance(); //5 } } } return mInstance; } ```

假如沒有用volatile,併發情況下會出現問題,執行緒A執行到註釋5 new TestInstance() 的時候,分為如下幾個幾步操作:

  • 1、分配記憶體
  • 2、初始化物件
  • 3、mInstance 指向記憶體

這時候如果發生指令重排,執行順序是132,執行到第3的時候,執行緒B剛好進來了,並且執行到註釋2,這時候判斷mInstance不為空,直接使用一個未初始化的物件。所以使用volatile關鍵字來禁止指令重排序。

👓 volatile 原理

JVM底層volatile是採用記憶體屏障來實現的,記憶體屏障會提供3個功能:

  1. 它確保指令重排序時不會把其後面的指令排到記憶體屏障之前的位置,也不會把前面的指令排到記憶體屏障的後面;即在執行到記憶體屏障這句指令時,在它前面的操作已經全部完成;
  2. 它會強制將快取的修改操作立即寫到主記憶體
  3. 寫操作會導致其它CPU中的快取行失效,寫之後,其它執行緒的讀操作會從主記憶體讀。

👓 volatile 的侷限性

volatile 只能保證可見性,不能保證原子性寫操作對其它執行緒可見,但是不能解決多個執行緒同時寫的問題。

👓 Synchronized

👓 使用場景

多個執行緒同時寫一個變數。

例如售票,餘票是100張,視窗A和視窗B同時各賣出一張票, 假如餘票變數用 volatile 修飾,是有問題的。\ A視窗獲取餘票是100,B視窗獲取餘票也是100,A賣出一張變成99,重新整理回主記憶體,同時B賣出一張變成99,也重新整理回主記憶體,會導致最終主記憶體餘票是99而不是98。

前面說到volatile的侷限性,就是多個執行緒同時寫的情況,這種情況一般可以使用Synchronized

Synchronized 可以保證同一時刻,只有一個執行緒可執行某個方法或某個程式碼塊。

👓 Synchronized 原理

```js public class SynchronizedTest {

public static void main(String[] args) { synchronized (SynchronizedTest.class) { System.out.println("123"); } method(); }

private static void method() { } } ```

將這段程式碼先用javac命令編譯,再java p -v SynchronizedTest.class命令檢視位元組碼,部分位元組碼如下

js public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: ldc #2 // class com/lanshifu/opengldemo/test/SynchronizedTest 2: dup 3: astore_1 4: monitorenter 5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 8: ldc #4 // String 123 10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 13: aload_1 14: monitorexit 15: goto 23 18: astore_2 19: aload_1 20: monitorexit 21: aload_2 22: athrow 23: invokestatic #6 // Method method:()V 26: return

可以看到 4: monitorenter14: monitorexit,中間是列印的語句。

執行同步程式碼塊,首先會執行monitorenter指令,然後執行同步程式碼塊中的程式碼,退出同步程式碼塊的時候會執行monitorexit指令 。

使用Synchronized進行同步,其關鍵就是必須要對物件的監視器monitor進行獲取,當執行緒獲取monitor後才能繼續往下執行,否則就進入同步佇列,執行緒狀態變成BLOCK,同一時刻只有一個執行緒能夠獲取到monitor,當監聽到monitorexit被呼叫,佇列裡就有一個執行緒出隊,獲取monitor。詳情參考:www.jianshu.com/p/d53bf830f…

每個物件擁有一個計數器,當執行緒獲取該物件鎖後,計數器就會加一,釋放鎖後就會將計數器減一,所以只要這個鎖的計數器大於0,其它執行緒訪問就只能等待。

👓 Synchronized鎖的升級

大家對Synchronized的理解可能就是重量級鎖,但是Java1.6Synchronized 進行了各種優化之後,有些情況下它就並不那麼重,Java1.6 中為了減少獲得鎖和釋放鎖帶來的效能消耗而引入的偏向鎖輕量級鎖

偏向鎖: 大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,為了讓執行緒獲得鎖的代價更低而引入了偏向鎖。

當一個執行緒A訪問加了同步鎖的程式碼塊時,會在物件頭中存 儲當前執行緒的id,後續這個執行緒進入和退出這段加了同步鎖的程式碼塊時,不需要再次加鎖和釋放鎖。

輕量級鎖: 在偏向鎖情況下,如果執行緒B也訪問了同步程式碼塊,比較物件頭的執行緒id不一樣,會升級為輕量級鎖,並且通過自旋的方式來獲取輕量級鎖。

重量級鎖: 如果執行緒A和執行緒B同時訪問同步程式碼塊,則輕量級鎖會升級為重量級鎖,執行緒A獲取到重量級鎖的情況下,執行緒B只能入隊等待,進入BLOCK狀態。

👓 Synchronized 缺點

  • 不能設定鎖超時時間
  • 不能通過程式碼釋放鎖
  • 容易造成死鎖

👓 ReentrantLock

上面說到Synchronized的缺點,不能設定鎖超時時間和不能通過程式碼釋放鎖,ReentranLock就可以解決這個問題。

在多個條件變數和高度競爭鎖的地方,用ReentrantLock更合適ReentrantLock還提供了Condition,對執行緒的等待和喚醒等操作更加靈活,一個ReentrantLock可以有多個Condition例項,所以更有擴充套件性。

👓 ReentrantLock 的使用

lock 和 unlock

js ReentrantLock reentrantLock = new ReentrantLock(); System.out.println("reentrantLock->lock"); reentrantLock.lock(); try { System.out.println("睡眠2秒..."); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); }finally { reentrantLock.unlock(); System.out.println("reentrantLock->unlock"); }

實現可定時的鎖請求:tryLock

```js public static void main(String[] args) { ReentrantLock reentrantLock = new ReentrantLock(); Thread thread1 = new Thread_tryLock(reentrantLock); thread1.setName("thread1"); thread1.start(); Thread thread2 = new Thread_tryLock(reentrantLock); thread2.setName("thread2"); thread2.start(); }

static class Thread_tryLock extends Thread {
    ReentrantLock reentrantLock;

    public Thread_tryLock(ReentrantLock reentrantLock) {
        this.reentrantLock = reentrantLock;
    }

    @Override
    public void run() {
        try {
            System.out.println("try lock:" + Thread.currentThread().getName());
            boolean tryLock = reentrantLock.tryLock(3, TimeUnit.SECONDS);
            if (tryLock) {
                System.out.println("try lock success :" + Thread.currentThread().getName());
                System.out.println("睡眠一下:" + Thread.currentThread().getName());
                Thread.sleep(5000);
                System.out.println("醒了:" + Thread.currentThread().getName());
            } else {
                System.out.println("try lock 超時 :" + Thread.currentThread().getName());
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("unlock:" + Thread.currentThread().getName());
            reentrantLock.unlock();
        }
    }
}

```

列印的日誌:

js try lock:thread1 try lock:thread2 try lock success :thread2 睡眠一下:thread2 try lock 超時 :thread1 unlock:thread1 Exception in thread "thread1" java.lang.IllegalMonitorStateException at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151) at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261) at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457) at com.lanshifu.demo_module.test.lock.ReentranLockTest$Thread_tryLock.run(ReentranLockTest.java:60) 醒了:thread2 unlock:thread2

上面演示了trtLock的使用,trtLock設定獲取鎖的等待時間,超過3秒直接返回失敗,可以從日誌中看到結果。 有異常是因為thread1獲取鎖失敗,不應該呼叫unlock

👓 Condition 條件

```js public static void main(String[] args) {

    Thread_Condition thread_condition = new Thread_Condition();
    thread_condition.setName("測試Condition的執行緒");
    thread_condition.start();
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    thread_condition.singal();

}

static class Thread_Condition extends Thread {

    @Override
    public void run() {
        await();
    }

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

    public void await() {
        try {
            System.out.println("lock");
            lock.lock();
            System.out.println(Thread.currentThread().getName() + ":我在等待通知的到來...");
            condition.await();//await 和 signal 對應
            //condition.await(2, TimeUnit.SECONDS); //設定等待超時時間
            System.out.println(Thread.currentThread().getName() + ":等到通知了,我繼續執行>>>");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("unlock");
            lock.unlock();
        }
    }

    public void singal() {
        try {
            System.out.println("lock");
            lock.lock();
            System.out.println("我要通知在等待的執行緒,condition.signal()");
            condition.signal();//await 和 signal 對應
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("unlock");
            lock.unlock();
        }
    }
}

```

執行列印日誌

js lock 測試Condition的執行緒:我在等待通知的到來... lock 我要通知在等待的執行緒,condition.signal() unlock 測試Condition的執行緒:等到通知了,我繼續執行>>> unlock 複製程式碼

上面演示了Condition的 await 和 signal 使用,前提要先lock。

👓 公平鎖與非公平鎖

ReentrantLock 建構函式傳true表示公平鎖。

公平鎖表示執行緒獲取鎖的順序是按照執行緒加鎖的順序來分配的,即先來先得的順序。而非公平鎖就是一種鎖的搶佔機制,是隨機獲得鎖的,可能會導致某些執行緒一致拿不到鎖,所以是不公平的。

👓 ReentrantLock 注意點

  1. ReentrantLock使用lockunlock來獲得鎖和釋放鎖
  2. unlock要放在finally中,這樣正常執行或者異常都會釋放鎖
  3. 使用conditionawaitsignal方法之前,必須呼叫lock方法獲得物件監視器

👓 併發包

通過上面分析,併發嚴重的情況下,使用鎖顯然效率低下,因為同一時刻只能有一個執行緒可以獲得鎖,其它執行緒只能乖乖等待。

Java提供了併發包解決這個問題,接下來介紹併發包裡一些常用的資料結構。

👓 ConcurrentHashMap

我們都知道HashMap是執行緒不安全的資料結構,HashTable則在HashMap基礎上,get方法和put方法加上Synchronized修飾變成執行緒安全,不過在高併發情況下效率底下,最終被ConcurrentHashMap替代。

ConcurrentHashMap 採用分段鎖,內部預設有16個桶,getput操作,首先將key計算hashcode,然後跟16取餘,落到16個桶中的一個,然後每個桶中都加了鎖(ReentrantLock),桶中是HashMap結構(陣列加連結串列,連結串列過長轉紅黑樹)。

所以理論上最多支援16個執行緒同時訪問。

👓 LinkBlockingQueue

連結串列結構的阻塞佇列,內部使用多個ReentrantLock

```js /* Lock held by take, poll, etc / private final ReentrantLock takeLock = new ReentrantLock();

/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();

/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();

/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();

private void signalNotEmpty() {
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
}

/**
 * Signals a waiting put. Called only from take/poll.
 */
private void signalNotFull() {
    final ReentrantLock putLock = this.putLock;
    putLock.lock();
    try {
        notFull.signal();
    } finally {
        putLock.unlock();
    }
}

```

原始碼不貼太多,簡單說一下LinkBlockingQueue 的邏輯:

  1. 從佇列獲取資料,如果佇列中沒有資料,會呼叫notEmpty.await();進入等待。
  2. 在放資料進去佇列的時候會呼叫notEmpty.signal();,通知消費者,1中的等待結束,喚醒繼續執行。
  3. 從佇列裡取到資料的時候會呼叫notFull.signal();,通知生產者繼續生產。
  4. 在put資料進入佇列的時候,如果判斷佇列中的資料達到最大值,那麼會呼叫notFull.await();,等待消費者消費掉,也就是等待3去取資料並且發出notFull.signal();,這時候生產者才能繼續生產。

LinkBlockingQueue 是典型的生產者消費者模式,原始碼細節就不多說。

👓 原子操作類:AtomicInteger

內部採用CAS(compare and swap)保證原子性

舉一個int自增的例子

js AtomicInteger atomicInteger = new AtomicInteger(0); atomicInteger.incrementAndGet();//自增

原始碼看一下

js /** * Atomically increments by one the current value. * * @return the updated value */ public final int incrementAndGet() { return U.getAndAddInt(this, VALUE, 1) + 1; }

U 是 Unsafe,看下 Unsafe#getAndAddInt

```js public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

  return var5;

} ```

通過compareAndSwapInt保證原子性。

👓 執行緒通訊

該部分摘抄自java中的多執行緒:執行緒使用、執行緒安全、執行緒通訊 \ 本人對於多執行緒相知不多,目前也在學習中,該部分暫時引用,等後續會加入自己的見解進行改寫 執行緒有自己的私有空間,但當我多個執行緒之間相互協作的時候,就需要進行執行緒間通訊方,本節將介紹Java執行緒之間的幾種通訊原理。

🎯鎖與同步

這種方式主要是對全域性變數加鎖,即用synchronized關鍵字對物件或者程式碼塊加鎖lock,來達成執行緒間通訊。

這種方式可詳見上一節執行緒同步中的例子。

🎯等待/通知機制

基於“鎖”的方式需要執行緒不斷去嘗試獲得鎖,這會耗費伺服器資源。

Java多執行緒的等待/通知機制是基於Object類的wait()方法和notify(), notifyAll()方法來實現的,

wait()方法和notify()方法必須寫在synchronized程式碼塊裡面:

wait()notify()方法必須通過獲取的鎖物件進行呼叫,因為wait就是執行緒在獲取物件鎖後,主動釋放物件鎖,同時休眠本執行緒,直到有其它執行緒呼叫物件的notify()喚醒該執行緒,才能繼續獲取物件鎖,並繼續執行。相應的notify()就是對物件鎖的喚醒操作,因而必須放在加鎖的synchronized程式碼塊環境內。

notify()方法會隨機叫醒一個正在等待的執行緒,而notifyAll()會叫醒所有正在等待的執行緒,被喚醒的執行緒重新在就緒佇列中按照一定演算法最終再次被處理機獲得並進行處理,而不是立馬重新獲得處理機。

```js public class mythread {

private static Object lock = new Object();

static class ThreadA implements Runnable {
    @Override
    public void run() {
        synchronized (lock) {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println("ThreadA: " + i);
                    lock.notify();
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            lock.notify();
        }
    }
}

static class ThreadB implements Runnable {
    @Override
    public void run() {
        synchronized (lock) {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println("ThreadB: " + i);
                    lock.notify();
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            lock.notify();
        }
    }
}


public static void main(String[] args) {
    new Thread(new ThreadA()).start();
    try {
        Thread.sleep(10000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    new Thread(new ThreadB()).start();


}

}

```

🎯join方法

join()方法讓當前執行緒陷入“等待”狀態,等join的這個執行緒執行完成後,再繼續執行當前執行緒。

當主執行緒建立並啟動了耗時子執行緒,而主執行緒早於子執行緒結束之前結束時,就可以用join方法等子執行緒執行完畢後,從而讓主執行緒獲得子執行緒中的處理完的某個資料。

join()方法及其過載方法底層都是利用了wait(long)這個方法。

```js public class mythread {

static class ThreadA implements Runnable {

    @Override
    public void run() {
        try {
            System.out.println("子執行緒睡一秒");
            Thread.sleep(1000);
            System.out.println("子執行緒睡完了一秒");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(new ThreadA());
    thread.start();
    thread.join();
    System.out.println("如果不加join方法,這行就會先打印出來");
}

} ```

🎯sleep方法

sleep方法是Thread類的一個靜態方法。它的作用是讓當前執行緒睡眠一段時間:

  • Thread.sleep(long)

這裡需要強調一下:sleep方法是不會釋放當前的鎖的,而wait方法會。這也是最常見的一個多執行緒面試題。

sleep方法和wait方法的區別:

  • wait可以指定時間,也可以不指定;而sleep必須指定時間。
  • wait釋放cpu資源,同時釋放鎖;sleep釋放cpu資源,但是不釋放鎖,所以易死鎖。
  • wait必須放在同步塊或同步方法中,而sleep可以再任意位置

🎯ThreadLocal類

ThreadLocal是一個本地執行緒副本變數工具類,可以理解成為執行緒本地變數或執行緒本地儲存。嚴格來說,ThreadLocal類並不屬於多執行緒間的通訊,而是讓每個執行緒有自己“獨立”的變數,執行緒之間互不影響。

ThreadLocal類最常用的就是set方法和get方法。示例程式碼:

```js public class mythread { static class ThreadA implements Runnable { private ThreadLocal threadLocal;

    public ThreadA(ThreadLocal<String> threadLocal) {
        this.threadLocal = threadLocal;
    }

    @Override
    public void run() {
        threadLocal.set("A");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("ThreadA輸出:" + threadLocal.get());
    }

    public static void main(String[] args) {
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        new Thread(new ThreadA(threadLocal)).start();
    }
}

} ```

可以看到,ThreadA可以存取自己當前執行緒的一個值。如果開發者希望將類的某個靜態變數(user ID或者transaction ID)與執行緒狀態關聯,則可以考慮使用ThreadLocal,而不是在每個執行緒中宣告一個私有變數來操作,加“重”執行緒。

InheritableThreadLocalThreadLocal的繼承子類,不僅當前執行緒可以存取副本值,而且它的子執行緒也可以存取這個副本值。

🎯訊號量機制

JDK提供了一個類似於“訊號量”功能的類Semaphore。在多個執行緒(超過2個)需要相互合作的場景下,我們用簡單的“鎖”和“等待通知機制”就不那麼方便了。這個時候就可以用到訊號量。JDK中提供的很多多執行緒通訊工具類都是基於訊號量模型的。

🎯管道

管道是基於“管道流”的通訊方式。JDK提供了PipedWriterPipedReaderPipedOutputStreamPipedInputStream。其中,前面兩個是基於字元的,後面兩個是基於位元組流的。

應用場景:管道多半與I/O流相關。當我們一個執行緒需要先另一個執行緒傳送一個資訊(比如字串)或者檔案等等時,就需要使用管道通訊了。

```js public class Pipe { static class ReaderThread implements Runnable { private PipedReader reader;

    public ReaderThread(PipedReader reader) {
        this.reader = reader;
    }

    @Override
    public void run() {
        System.out.println("this is reader");
        int receive = 0;
        try {
            while ((receive = reader.read()) != -1) {
                System.out.print((char)receive);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

static class WriterThread implements Runnable {

    private PipedWriter writer;

    public WriterThread(PipedWriter writer) {
        this.writer = writer;
    }

    @Override
    public void run() {
        System.out.println("this is writer");
        int receive = 0;
        try {
            writer.write("test");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

public static void main(String[] args) throws IOException, InterruptedException {
    PipedWriter writer = new PipedWriter();
    PipedReader reader = new PipedReader();
    writer.connect(reader); // 這裡注意一定要連線,才能通訊

    new Thread(new ReaderThread(reader)).start();
    Thread.sleep(1000);
    new Thread(new WriterThread(writer)).start();
}

}

// 輸出: this is reader this is writer test ```

致謝