--- title: 'Java 多執行序:同步、鎖、ThreadLocal' disqus: kyleAlien --- Java 多執行序:同步、鎖、ThreadLocal === ## Overview of Content 如有引用參考請詳註出處,感謝 :smile: > 介紹 `Synchronized` 同步、`Volatile`;以下可能會混用 “線程”、“執行序”,兩者是相同意思 :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**全面解析多執行緒與同步技術:SYNC、CAS、ThreadLocal**](https://devtechascendancy.com/atomic-visibility-ordering-volatile-aqs/) ::: [TOC] ## 多執行序:安全概述 多執行序是常見可以加強 CPU 吞吐量的手段,可以有效提高效率(正確使用的話),但由於 **多執行緒是程式設計提供的概念(並且每種語言的實現並不同),它不具備保證資源存儲的安全性!** > 多執行序的不安全是在 **對同個物件進行寫入 (改變)** 資源時(讀取則不會有安全問題) :::success * 這裡談及的是「**執行序, `Thread`**」而不是程序 程序(也就是「應用」)是核心系統提供的功能,核心系統會將其稱之為 `Process`,它就具備資源訪問安全性,它讓每個應用都活在虛擬記憶體中不會相互干擾 ::: ### 執行緒安全:考量點 * **多執行序安全往往是以「性能」作為代價(鎖)**,因此我們使用鎖 🔒 需考慮到以下幾點 * 只對可能產生資源搶奪的區塊進行代碼同步,同步的代碼越長效能越差,因為要做更多的等待 如果沒有安排的隨意使用執行緒,那代碼的執行效率反而會不升反降 * 我們也可以在創建類時區分 「單執行緒執行環境」、「多執行緒執行環境」,避免統一用多執行環境的物件(這是一種開發上的約束) ## Java synchronized 同步 * `synchronized` 是 Java 語言在使用同步時的一個「**修飾符**」,一次只讓一個執行序訪問,**++一個物件讓多個執行序訪問++**;以下是幾個 Java `synchronized` 修飾符使用時要注意的事情… :::warning * **可以 `Synchronized` Null 物件** ? **不行 !** Synchronized 一定是**針對某一物件做出同步動作**,所以一定要有物件(要有物件可以搶,不然就無法同步了) ::: :::danger * **`synchronized` 效果可以繼承** ? **不行 !** **==Override 不可繼承 synchronized 效果==**,繼承時 **要自己加入 `synchronized` 關鍵字**,但是可以透過 super(呼叫父類的同步方法) > 以下案例中來證明 synchronized 不會作用在繼承 > `TestSynch` 子類覆寫 `addData` 方法但並沒有加上同步,就沒有同步功能 > > ![](https://i.imgur.com/FTRs5Ht.png) ::: ### synchronized 方法:自動鎖定 * 這裡的自動鎖動,是指使用 `synchronized` 時不去指定物件,而使用預設的 `this` 物件 **認識 `this`**:每一個 **物件都有內含 `this` 關鍵字**,**它代表的是這個物件的 `instance` 實例**,它是個隱藏物件 ```mermaid graph LR subgraph Hello_instance t(this 物件) end c(class Hello) -.-> |實例化| Hello_instance ``` * 接下來我們 **使用 `synchronized` 關鍵字來同步方法,就是手法鎖定 `this` 物件**,**鎖的物件是目前的實例**(也就是 `this`);接下來凡事使用到該物件的地方都需要做等待 :::danger 當然,請特別注意它鎖定的是「實例」,也就是 **多個執行序都要訪問同一個實例,那才有所定的功能**;如果多個執行序訪問不同的實例,那就沒有同步的效果 ::: ```java= // 同步方法 synchronized void addData() { // TODO: } // 區塊同步,鎖的物件同上,下一小節會說明… void addData() { synchronized(this) { // TODO: } } ``` **--實作--** > 上面的範例 15 個太多 (不方便觀察),改為 5 個;可以看到使用兩個 Thread 訪問同一個物件的方法,會依照順序訪問 ! > > ![](https://i.imgur.com/2l4r8Nh.png) ### synchronized 同步區塊:指定同步物件 * `synchronized` 除了上述的 `this` 物件可以鎖定之外,也允許我們指定一個物件來鎖定(也可以是 `this`,因為自身就是一個物件),持有此物件的 Thread 才可執行同步的函數 :::info * 透過指定物件而不使用 `this` 的好處在於,**可以更精細的去調整同步,可以有效的提高效率** > 也就是 **可以在一個物件中使用多個「物件(也就是鎖)」,而不是全部執行序都搶 `this` 這把鎖** ::: * **`synchronized` 指定 `this` 物件來鎖定**,效果跟「synchronized 方法」是一樣的 ```java= public void addData() { synchronized(this) { //TODO: } } } ``` * **`synchronized` 物件指定自己創建物件來鎖定** ```java= private Object o = new Object(); // 一定是實例化,不實例化無法作為 key // or // private byte[] b = new byte[0]; // byte[] array 比 Object 需要更小的範圍 public void addData() { synchronized(this) { //TODO: } } } ``` :::danger * 但這個 **物件(鎖)一定要實例化,不可以為 null**,否則會報錯,因為 `synchronized` 鎖的是一個物件 > ![](https://i.imgur.com/isvx9AW.png) ::: **--實作--** > ![](https://i.imgur.com/v6UGM2a.png) :::success * **接下來的程式,我們再次強調「鎖定區塊」的特性**: 看看區塊同步鎖,是否只同步區塊內部(鎖定的區域),而非區塊內的程式不同不 ```java= public class HelloWorld { public static void main(String []args){ HelloWorld h = new HelloWorld(); new Thread( h::testPrint ).start(); new Thread( h::testPrint ).start(); } private void testPrint() { synchronized(this) { // 同步區塊開始 for(int i = 0; i <5; i++) { System.out.println(Thread.currentThread().getName() + ", test: " + i); } } // 同步區塊結束 // 故意不同步一行程式 System.out.println("\n" + Thread.currentThread().getName() + ", Finish"); } } ``` 從下圖可見,可以看到 for 回圈內的行為會同步,但是 最後的 `println` 資訊則是 Thread 互搶!因為這行程式沒有被同步!! > ![](https://i.imgur.com/3HrzbdX.png) ::: ### synchronized 同步 - 靜態方法 / 靜態物件 / 類 * 由於 **靜態物件在 JVM 虛擬機中只存在一個物件**,所以是指同一把鎖,**同步 Class 也是相同的意思,因為 Class 在虛擬機中也只存在一個物件**! > 想解解 Class 最好認識一下 ClassLoader,[**ClassLoder**](https://devtechascendancy.com/class-lifecycle_classloader-exploration_jvm/#Java_ClassLoader_%E7%89%B9%E9%BB%9E) 知識請點連結 :::info * **JVM 內存模型中,靜態方法、物件物件、類 (`Class`) 都是存在不同地方** | 目標 | 內存模型儲存位置 | JVM 中物件數量是否單一 | | - | - | - | | 靜態方法 | 方法區 | Y | | 靜態物件 | 靜態變量區 | Y | | 物件(`instance`) | 堆區 | N | ::: ```java= // 同步靜態方法 public synchronized static void addData() { for(int i = 0; i < 15; i++) { x++; System.out.println(Thread.currentThread().getName() + ", x: " + x); } } // 同步靜態物件 private static int x = 0; private static Object o = new Object(); public void addData() { synchronized(o) { for(int i = 0; i < 15; i++) { x++; System.out.println(Thread.currentThread().getName() + ", x: " + x); } } } // 同步 class類物件 public void addData() { synchronized(TestSynch.class) { for(int i = 0; i < 15; i++) { x++; System.out.println(Thread.currentThread().getName() + ", x: " + x); } } } ``` * 範例:創建了兩個物件 `s1` & `s2`,用不同線程 `t1` & `t2` 訪問不同物件 * **同步 `靜態方法`**:由於是靜態方法,所有是所有物件共用的方法,所以不同物件也仍會做等待的行為 > ![](https://i.imgur.com/h7WRO83.png) * **同步 `靜態物件`**:其實仍相同,由於使用 static 物件同步,所以可以達到相同的效果 > ![](https://i.imgur.com/OdWK13A.png) * **同步 `Class` 類**:同上,由於在 JVM 內一個物件只會有一個 Class 物件,所以可以等同於同步靜態物件 > ![](https://i.imgur.com/11knf0x.png) ## 多執行序之間協做 一個任務可以交給多個 Thread 運行,如果操作得當可以加快運行速度; ### wait 方法與 notify / notifyAll * 如同開關訊號,可用來等待或通知,**等待或通知方是使用 `同一把鎖`**;這三個方法必須 **使用在 synchronized 之下 (同把鎖)** | 方法 | 說明 | | -------- | -------- | | wait() | **`釋放物件鎖`**,並讓 Thread 進入等待狀態 | | wait(int) | 預設單位為 ms,時間過後喚醒 | | wait(long, int) | `會釋放物件鎖`,等待附加時間條件 | | notify() | 通知`隨機一個`持有鎖的物件,從 wait 到 work 狀態 | | notifyAll() | 通知`所有`持有鎖的物件,從 wait 到 work 狀態 | :::warning * 注意:喚醒是透過物件(鎖)來喚醒,並且喚醒時(`notify`/`notifyAll`)是要同一個鎖的物件來喚醒,**不同物件(鎖)不能被通知**! ::: ```java= public class startThread1 { public static void main(String[] args) { TestClass t = new TestClass("Alien"); //t.run(); "1. " t.start(); for(int i = 0; i < 17; i++) { t.setTime(i); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } } class TestClass extends Thread { private String name; private int time = 0; TestClass(String name) { this.name = name; } void setTime(int time) { synchronized(this) { // "2: " this.time = time; System.out.println("Now time is " + time); this.notifyAll(); } } @Override public void run() { synchronized(this) { // "3: " while(time < 8) { try { System.out.println("Before wait"); this.wait(500); // "4:" System.out.println("After wait"); } catch (InterruptedException e) { e.printStackTrace(); } } /* if(time < 8) { // "5: " try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } */ System.out.println("Till work time, "+ name + " go to work"); } } } ``` 1. 使用 **`run()` 會變成排隊執行**(`main Thread wait work Thread`),導致程式無法正常運作 2. **必須持有「同一把物件(鎖)」**,如果持有不同鎖則無法正常喚醒目標物件;`this` 是指 TestClass 物件鎖(並非類鎖) 3. Java 的每一個物件都可以作為鎖 (Object) 4. `wait()` 內如果設置時間 (單位預設為ms),就是限制等待時間,等待時間一過就自動 喚醒;如下圖可以看到,**1s 喚醒一次,但 wait 只休息 500ms,所以在 while 內喚醒了 2 次**,所以判斷了兩次 (time 相同) > ![](https://i.imgur.com/vgaUMBX.png) :::warning * **使用 `wait()` 要搭配 `while()` 判斷式** 使用 if,可以看出 **notify 後會從 wait 休眠的行數 繼續執行** > ![](https://i.imgur.com/H2tweM0.png) ::: ### 鎖的釋放時機 * 鎖的釋放時機:一般來說有以下幾個時機會釋放鎖 * 執行完同步程式碼區塊 * **出現例外狀況(拋出異常)導致執行序被終止,鎖就會自動釋放**(這很重要,避免程序造成死鎖無法正常運行) * **Object#`wait` 方法**:這個方法很特別,它會將當前執行緒掛起(Blocking),並且釋放鎖,讓其他執行序執行該方法! :::info * Thread 的 sleep 方法 & Object 的 wait 方法兩者個區別 | 比較點 | Thread#sleep | Object#wait | | -------- | -------- | -------- | | 釋放 CPU 資源 | Y | Y | | 重入時,是否釋放鎖(重入鎖下個小節說明) | 不釋放 | 釋放鎖 | | 是否需要喚醒 | 不需要,只會睡眠規定時間 | 可以持續等待直到被喚醒 | | 區域性 | 無 | 必須在 `synchronized` 區塊內使用 | ::: ## ReentrantLock 機制:認識更多不同的鎖 在 Java 中,ReentrantLock 是一種提供 **可重入鎖**(`reentrant lock`)功能的鎖實現,屬於 `java.util.concurrent.locks` 包 ReentrantLock 提供了比 `synchronized` 關鍵字更靈活的鎖機制,允許更細粒度的鎖控制,並提供了一些額外的功能 * **ReentrantLock 的主要特性** 1. **可重入性**:如果一個執行序已經獲得了鎖,可以再次獲得鎖而不會被阻塞(`synchronized` 也同樣是可重入) 2. **公平鎖**:ReentrantLock 可以配置為公平鎖,確保等待時間最長的執行序最先獲得鎖 > 默認情況下是非公平鎖,因為非公平鎖的效能較加 3. **可中斷鎖**:等待鎖的執行序可以被中斷 4. **嘗試獲取鎖**:可以嘗試在獲取鎖時設置超時,防止長時間等待 5. **提供條件變量**:ReentrantLock 可以產生多個 Condition 對象,實現更複雜的執行序間同步 ### 認識可重入鎖 * 鎖是否可重入的行為在「遞歸」的程式設計中相當重要;在「不可重入鎖」中使用遞歸會造成程式卡死… 而在「可重入鎖」**遞歸調用時,可以重新獲得鎖**,再次進入執行程式 遞歸的程式設計概念如下 ```java= // 概念程式 synchronized int showData(int x) { if(x <= 0) { return 0; } return 1 + showData(x - 1); // 遞歸呼叫,重入同步方法 } ``` * **==synchronized 默認可支持重入鎖==**,如果非重入鎖,就會發生死鎖,因為一直在等待開鎖,**使用 ++遞歸就可證明++ synchronized 是重入鎖** ```java= synchronized int showData(int x) { System.out.println("x: " + x); x--; if(x == 0) { System.out.println("Reach zero"); return x; } return showData(x); // 遞歸定調用 } ``` ### 認識顯式鎖 & 隱式鎖 * **隱式鎖**:**synchronized 是隱式鎖又稱為內置鎖**,因為它的加鎖、解鎖功能都不會顯示在程式中,可掌控度較低,但使用起來較為簡單 使用 synchronized 關鍵字時,**鎖的獲取和釋放是由 Java 虛擬機自動處理的** ```java= public synchronized void someMethod() { // 此方法由隱式鎖保護 } public void someMethod() { synchronized (this) { // 這段代碼由隱式鎖保護 } } ``` 當執行序進入 `synchronized` 區塊或方法時,自動獲取鎖;當執行序離開 `synchronized` 區塊或方法時,自動釋放鎖 * **隱式鎖**:**ReentrantLock 是顯式鎖**,因為它的加鎖、解鎖都要開發者自己來操作,所以 **把解鎖放在 finally 中很重要** * **明確的加鎖和解鎖**:使用 ReentrantLock 時,需要明確地調用 lock() 方法來獲取鎖,並在不需要鎖時調用 unlock() 方法來釋放鎖… ```java= private final ReentrantLock lock = new ReentrantLock(); public void someMethod() { lock.lock(); try { // 這段代碼由顯式鎖保護 } finally { lock.unlock(); } } ``` 在 try 區塊內進行需要同步的操作,並在 finally 區塊中確保鎖被釋放,即使在出現異常的情況下也能保證鎖被釋放,避免死鎖 * **高控制度**:ReentrantLock 提供了更多功能和更高的控制度,比如可中斷鎖、嘗試獲取鎖以及公平鎖的支持,使得其在需要更複雜同步機制的情況下更具優勢 * **條件變量**:ReentrantLock 提供了 Condition 物件,用於實現更複雜的執行序間協調 ```java= private final ReentrantLock lock = new ReentrantLock(); private final Condition condition = lock.newCondition(); private boolean ready = false; public void awaitMethod() throws InterruptedException { lock.lock(); try { while (!ready) { condition.await(); } // 處理 ready 為 true 時的情況 } finally { lock.unlock(); } } public void signalMethod() { lock.lock(); try { ready = true; condition.signalAll(); } finally { lock.unlock(); } } ``` ### ReentrantLock 常用方法 * synchronized & ReentrantLock 鎖的差異如下表 | synchronized | ReentrantLock | 解釋 | | -------------- | ------------------- | ------------------------------------------------ | | `synchronized()` | `lock()` | 獲取鎖 | | **沒辦法** | `tryLock()` | **用非阻塞的方法**,嘗試獲取鎖,並返回 | | **沒辦法** | `lockInterruptibly()` | 跟 lock 的差別在,**獲取鎖的過程中可以響應中斷** | | 自動釋放 | `unlock()` | 釋放鎖 | | `wait()` | `await()` | 等待**並釋放鎖** | | `notify()` | `signal()` | **隨機喚醒**持有鎖的物件 | | `notifyAll()` | `signalAll()` | **喚醒全部**持有鎖的物件 | ### ReentrantLock 使用、[Condition ](https://developer.android.com/reference/java/util/concurrent/locks/Condition) 條件控制 * ReentrantLock 也可以達到跟 synchronized 相同的同步效果,而要保障的安全同步操作區需要在獲取鎖(`lock`)、釋放鎖(`unlock`)之間執行 * 另外說到喚醒,ReentrantLock 自身就可以喚醒鎖… 而且 **ReentrantLock 也提供條件控制類 `Condition` 來達到更細節的控制** ReentrantLock 可以取得多個 Condition 物件,未來可以用在 **不同的條件喚醒**,使用 **一個 condition 對應一個鎖的條件可達到更精準的控制**(對於效能也有一定的改善) :::warning **Conditoin 的操作必須在 lock & unlock 之間** ::: ```java= class TestRETL implements Runnable { private ReentrantLock lock = new ReentrantLock(); private Condition c = lock.newCondition(); private int a = 0; public void compareData() { lock.lock(); try { while(a < 8) { try { System.out.println("before await"); c.await(); System.out.println("after await");; } catch (InterruptedException e) { e.printStackTrace(); } } } finally { lock.unlock(); } System.out.println("a is 10"); } @Override public void run() { compareData(); } public void addValue() { lock.lock(); try { a++; c.signal(); } finally { lock.unlock(); } } } ``` **--實做--** > ![](https://i.imgur.com/Iq4A8q4.png) ### 認識公平鎖 & 非公平鎖 * 公平鎖就是依照時間去排序,去執行 (**公平鎖的獲取是順序的**) * 非公平鎖的的獲取是「**搶順序**」 * ReentrantLock 有一個構造函數,可以控制是否是公平鎖 (**`ReentrantLock` 預設是非公平鎖,`synchronized` 預設就是非公平鎖**) 在一般開發中較少使用公平鎖,如果有需要,也可以使用 ReentrantLock 獲得公平鎖 ```java= Lock lock = new ReentrantLock(true); // 設置為公平鎖 ``` :::success * **非公平鎖的效率較高**,為甚麽? 假設喚醒執行序要 1000us 休眠需要 1000us,現在有 A、B、C 執行序,A 搶到鎖(假設鎖的流程 A-B-C),B 休眠 3s,C 休眠 1s 1. 依照公平鎖,執行順序就是 A-B-C,C 醒後 1000us 會在休眠 1000us,這樣 C 就要喚醒在睡眠花 2000us (因為要等 B 結束) 2. 如果是非公平鎖,執行順序就是先醒 C 的可先搶到資源,並在B 醒之前釋放鎖,這樣效率更高 ::: ### 認識死鎖 & 活鎖 * 死鎖指的是,**兩個以上的執行序(`m >= 2`), 搶奪兩個以上的資源(`n >= 2`)**,並且死鎖在學術上也有 4 個定義,如下所述 1. **請求 & 持有** : 當一個執行序持有一個資源後將其保持,並開始請求下一個資源 2. **互斥** : 當一個資源只能有一個執行序持有,當持有後就會鎖住不讓其他執行序獲取該資源 3. **不剝奪** : 當一個執行序獲得資源後,在為完成任務之前是不能被剝奪權力的,只能透過該資源自己釋放 4. **環狀等待** : A 等待 B 釋放資源,但 B 又在等 A 釋放資源 而這些死鎖上的定義也有相對的處理方式 | 狀態 | 處理方式 | | - | - | | 請求 & 持有 | 執行序運行前先獲取所有資源,沒有獲得所需全部資源則不運行 | | 互斥 | 修改獨佔資源改為虛擬資源,類似於指標物件,但大部分已無法修改 | | 不剝奪 | 當一個執行序持有一個資源後,在去獲取另一個物件失敗時,就連之前的資源一起釋放 | | 環狀等待 | 所有進程只能按照編號申請資源 | * **解決死鎖的關鍵在,==拿鎖的順序一致==**,以下我們來試試看死鎖、解除死鎖的方式 從下面的範例可以看它們 **取鎖的順序是相反不同的(兩個執行序對 `Lock_1`、`Lock_2` 兩個鎖的順序不同),所以會導致死鎖,如果順序相同就不會產生死鎖 (如果改為相同順序就可以正常取鎖)** ```java= // 死鎖範例 public class DeadLock { private static Object Lock_1 = new Object(); private static Object Lock_2 = new Object(); public static void main(String[] args) { new AlienTask().start(); new KyleTask().start(); System.out.println("Both Finish Task"); } private static class AlienTask extends Thread { @Override public void run() { synchronized(Lock_1) { System.out.println("Alien get Lock_1"); try { Thread.sleep(100); // 睡眠 100ms 讓其他執行序搶 } catch (InterruptedException e) { e.printStackTrace(); } synchronized(Lock_2) { System.out.println("Alien get Lock_2"); } } } } private static class KyleTask extends Thread { @Override public void run() { synchronized(Lock_2) { System.out.println("Kyle get Lock_2"); try { Thread.sleep(100); // 睡眠 100ms 讓其他執行序搶 } catch (InterruptedException e) { e.printStackTrace(); } synchronized(Lock_1) { System.out.println("Kyle get Lock_1"); } } } } } ``` > ![](https://i.imgur.com/dcZIhRR.png) :::info * 如果我們把鎖的順序對調,也就是將兩個執行序取鎖的順序調整為一樣順序,那就可以解開這個死鎖 > ![](https://i.imgur.com/VP5iHhs.png) ::: * **活鎖**:代表 **不斷的嘗試拿取鎖,並且當無法獲得鎖時就釋放鎖**,範例如下… > 採用嘗試拿鎖的機制,Lock's tryLock 方法等等 ```java= // 活鎖範例 public class AliveLock { public static Lock lock_1 = new ReentrantLock(); public static Lock lock_2 = new ReentrantLock(); public static void main(String[] args) { new AlienTask_1().start(); new KyleTask_1().start(); } public static class AlienTask_1 extends Thread { @Override public void run() { while(true) { if(lock_1.tryLock()) { System.out.println("Alien get One Lock"); try { if(lock_2.tryLock()) { try { System.out.println("Alien get Two Lock"); break; } finally { lock_2.unlock(); } } } finally { lock_1.unlock(); // 獲取 lock_2 不成功連 lock_1 都釋放 } } else { System.out.println("Alien get Key failed"); } } } } public static class KyleTask_1 extends Thread { @Override public void run() { while(true) { if(lock_2.tryLock()) { System.out.println("Kyle get One Lock"); try { if(lock_1.tryLock()) { try { System.out.println("Kyle get Two Key"); break; } finally { lock_1.unlock(); } } } finally { lock_2.unlock(); } } else { System.out.println("Kyle get Key failed"); } } } } } ``` **--範例--** > ![](https://i.imgur.com/a2xMZx8.png) ### 樂觀鎖 & 悲觀鎖 * **悲觀鎖**:`synchronized` 即為悲觀鎖,**不管有沒有搶資源都依率鎖住,這樣會導致效能下降** (好像總有人要跟它搶鎖,不管 3721 先鎖住資源就對了) * **樂觀鎖**:像是 CAS 就是樂觀鎖 (下面會在介紹),**複製一份副本,先進行作業,如果比對後發現不同於原來副本,則再次複製,一直循環到成功為止** :::success * **以效率來說是 ++樂觀鎖的效率會比較高++** 因為當執行序休眠需要 `10000` ~ `20000` 個指令時間,而 cpu 假設執行一個指令需要 `0.6u` 時間,那休眠 + 喚醒一次所花的時間,也就是 `20000 * 0.6u * 2 = 24ms`,**當執行一次作業不需要這麼長時間時就是樂觀鎖效率較高** ::: ## CAS 原子操作 全名是 `Compare And Swap`(比較和交換),這是 **使用 CPU 的特殊指令集,它會確保該操作是連貫動作「不可再切割」**(有就是動作的最小單位) **使用 CAS 操作時,每個執行序都會取得目標值的「副本」,在進行操作後會先比較原來的值是否為當初複製的值,如果不是則重新複製,重新操作**(樂觀鎖) > ![](https://i.imgur.com/iEli0T8.png) :::info 效率由低至高:**`Synchronized` < `ReentrantLock` < `CAS`** ::: ### 注意 CAS 的問題 * CAS 主要要注意三個問題,在考量這三個問題後再決定是否要用 CAS、或是直接改為同步 1. **ABA 問題** : CAS 會在放入數值後比對,如果數值如同剛複製的值就設定,否則就再次取值,要考慮到中途可能已經被修改過,然後再次改過來 :::info 簡單來說就是中間被修改過也不會知道 ::: > 複製值 A,執行操作到 C,放回去比對,也比對為 A 可設定 (但是必須考量到,比對的 A 可能中間已經被修改過了 A -> B -> A) > > ![](https://i.imgur.com/TkcaByY.png) 2. **循環時間開銷大** : 如果一直設定失敗會造成 CPU 負擔,並且花費時間也長,需要衡量 3. **只能保證原子操作** : 如果操作 2 個以上的元素則不保證這 2 個元素都是原子操做,**但是可以將 2 個以上的元素包裝成為一個類,對該類進行操作就可以保證原子性** ### JDK 中的原子操作 * JDK 提供的 CAS 操作主要分為三種,從它們的 Function name 就可以看出要它是如何操作的,如下表所示… | 類 | 功能 | Function 舉例 | | - | - | - | | `AtomicInteger` | 對 int 進行原子操做、當然也有 AtomicBoolean、AtomicLong...等等 | `addAndGet(int <輸入變數相加後返回>)`,`getAndSet(設置新值再返回舊值)`,`compareAndSet(int<期望值>, int<新值>)`、`getAndIncrement(內部變數加一並返回)` | | `AtomicIntegerArray` | 以原子方式更新數組 | `addAndGet(int<引索>, int<數值相加並返回>)`,`compareAndSet(int<引索>, int<期望值>, int<新值>)` | | `AtomicReference` | 原子數據操作類 | `compareAndSet` 比較&交換 | | `AtomicStampedReference` | 上面有提到 ABA 的問題就可以用這個解決,**該方法是使用 int 來計數原子的操作次數** | - | | `AtomiMarkableReference` | 同上方法功能,不過較為簡單,**使用 boolean mark,只關心是否有被動過** | - | 1. **AtomicInteger 的使用範例** ```java= public class AtomicNormal { public static void main(String[] args) { Thread[] ts = new Thread[3]; for(int i = 0; i < 3; i++) { ts[i] = new Atomic_Task_1(); } for(int i = 0; i < 3; i++) { ts[i].start(); } } static class Atomic_Task_1 extends Thread { static AtomicInteger ai = new AtomicInteger(10); @Override public void run() { int i = ai.getAndAdd(2); // Like a++,返回舊值 System.out.println("Name: " + Thread.currentThread().getName() + ", Old value: " + i + ", now value: " + ai.addAndGet(3)); // ++a,返回新值 } } } ``` **--實作結果--** > ![](https://i.imgur.com/2ddZM4E.png) 2. **AtomicReference 的使用範例** ```java= public class AtomicObject { public static void main(String[] args) { Thread[] ts = new Thread[3]; for(int i = 0; i < 3; i++) { ts[i] = new Atomic_Task_2(new InfoTable(i, i + 10)); } for(int i = 0; i < 3; i++) { ts[i].start(); } } static class Atomic_Task_2 extends Thread { static AtomicReference<InfoTable> af = new AtomicReference<>(new InfoTable(9527, 24)); InfoTable temp; Atomic_Task_2(InfoTable i) { temp = i; } @Override public void run() { // 執行到過為止 while(!af.compareAndSet(af.get(), temp)); // <比較值> <新值> InfoTable now = af.get(); System.out.println(now.toString()); } } private static class InfoTable { private long id; private int age; InfoTable(long id, int age) { this.id = id; this.age = age; } @Override public String toString() { return "id: " + id + ", age: " + age; } } } ``` **--實作結果--** > ![](https://i.imgur.com/CMCs7Ee.png) 3. **AtomicStampedReference 的使用範例** ```java= public class AtomicStamp { static AtomicStampedReference<String> asr = new AtomicStampedReference<>("Pan", 0); public static void main(String[] args) throws InterruptedException { final int oldStamp = asr.getStamp(); final String oldRef = asr.getReference(); System.out.println("old refernece: " + oldRef + ", stamp " + oldStamp + "\n"); Thread task_1 = new Thread(new Runnable() { @Override public void run() { System.out.println("Task_1" + ", ref: " + oldRef + ", Stamp: " + oldStamp + " - " + asr.compareAndSet(oldRef, oldRef + " Hello", oldStamp, oldStamp + 1)); } }); Thread task_2 = new Thread(new Runnable() { @Override public void run() { String reference = asr.getReference(); System.out.println("Task_2" + ", ref: " + reference + ", Stamp:" + asr.getStamp() + " - " + asr.compareAndSet(reference, reference + " World", oldStamp, oldStamp + 1)); } }); task_1.start(); task_2.start(); Thread.sleep(10); System.out.println("\nnow refernece: " + asr.getReference() + ", stamp " + asr.getStamp()); } } ``` **--實作結果--** > ![](https://i.imgur.com/X995RzN.png) ## ThreadLocal 執行序隔離 ThreadLocal 是使用執行序來隔離,內部使用了 `Map<Key, Value>`,**`Key` 為執行序,`Value` 為自己設定的數值**,**==可以用來隔離共享變數的操作==**,讓每個執行序都擁有一個自身變量的操作,不會相互影響 :::danger * **`ThreadLocal` 本身並不是用來保障同步行為的**! **它的主要目的是為每個執行序提供獨立的變量副本,從而避免多執行序環境下的變量共享問題**… 這種設計可以避免執行序之間的數據競爭,從而簡化多執行序編程中的數據隔離問題 ::: ### ThreadLocal 簡單範例 * 以下我們就來在多執行序的狀況下,透過 ThreadLocal 隔離變量,讓每個執行序自身安全的操作一個共享變量(正確點來說是共享變量的副本) ```java= // 使用 ThreadLocal public class ThreadLocalExample { // 創建 ThreadLocal,並初始化共享變量 private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0); public static void main(String[] args) { for (int i = 0; i < 3; i++) { new Thread(new Task()).start(); } } static class Task implements Runnable { @Override public void run() { int value = threadLocal.get(); value++; threadLocal.set(value); System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get()); } } } ``` 在這個例子中,ThreadLocal 保證每個執行序有自己獨立的 Integer 變量,並且每個執行序修改自己的 Integer 變量而不影響其他執行序 :::success * ThreadLocal 簡單來說就是把共享的變數,拷貝原型後後制到目前所在的執行序中,**讓每一個 Thread 都擁有此變數,而該變數互不相干** ThreadLocal 的內部實做是 **++將靜態變數儲存到 `ThreadLocalMap` 普通變數,並存於每個 Thread 中++**,**每一個 Thread 都訪問自己的 `ThreadLocalMap`** 來操作這個變數(這樣就是安全的操作) ::: **--實做--** > ![image](https://hackmd.io/_uploads/SJKHU1JVC.png) ### 手做 ThreadLocal 機制 * 自己 **實踐一個與 ThreadLocal 相同效果的類,它針對每一個執行序給予執行序自身的變量**,來達到相同的安全操作(請注意,這邊不是指安全的同步,而是指安全的隔離操作變量) 其中的重點有兩個 1. 透過 Map 把執行序作為 `Key` 來保存,並且在設置、取值的時候都透過 `Thread.currentThread()` 來取出當前的執行序 2. 需要一把鎖,來鎖定對於 `set`、`get` 的操作,來保證多執行序對於 Map 的同步操作(以下用自己創建鎖的方式) ```java= // 手做 ThreadLocal import java.util.HashMap; import java.util.Map; class MyThreadLocal<T> { private final Object lock = new Object(); private T initVal; private Map<Thread, T> maps = new HashMap<>(); public MyThreadLocal(T initVal) { this.initVal = initVal; } public void set(T t) { synchronized (lock) { Thread thread = Thread.currentThread(); maps.put(thread, t); } } public T get() { synchronized (lock) { Thread thread = Thread.currentThread(); T value = maps.get(thread); if (value == null) { return initVal; } return value; } } } public class HandlerThreadLocal { private static MyThreadLocal<Integer> threadLocal = new MyThreadLocal<>(0); public static void main(String[] args) { for(int i = 0; i < 3; i++) { new Thread(new MyHandlerTask()).start(); } } static class MyHandlerTask implements Runnable { @Override public void run() { int value = threadLocal.get(); value++; threadLocal.set(value); System.out.println(Thread.currentThread().getName() + ", hThreadLocal : " + threadLocal.get()); } } } ``` **--實做--** > ![image](https://hackmd.io/_uploads/S1cRFJy40.png) ### ThreadLocal 源碼分析 * 從 ThreadLocal 開始分析,會發現 ThreadLocal & Thread 有關係,**每一個 Thread 中都會儲存一個 ThreadLocalMap 物件** > ![](https://i.imgur.com/ZGvl5oi.png) 以下我們來看看 ThreadLocal#get 操作 ```java= // ThreadLocal 源碼的 get() 方法 // Thread 元素 ThreadLocal.ThreadLocalMap threadLocals = null; // "2. " public T get() { //"1. " Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue(); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } // ThreadLocal 內部類 ThreadLocalMap 的內部類 Entry static class Entry extends WeakReference<ThreadLocal> { Object value; Entry(ThreadLocal k, Object v) { super(k); value = v; } } // ThreadLocal getEntry(ThreadLocal) 方法 private Entry getEntry(ThreadLocal key) { int i = key.threadLocalHashCode & (table.length - 1); // "3. " Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); } ``` 1. 從這裡可以看得出來它是使用 currentThread 當作 key,但是 **ThreadLocal 並不是使用 Map 來儲存,而是 TheadLocalMap 這個物件** 2. 每一個 Thread 物件中都有 ThreadLocalMap 屬性,該物件也不是靜態物件,當該物件為空時就在內部創建 ThreadLocalMap,而 **ThreadLocalMap 內部有一個屬性 `Entry[]` 數組** > `private Entry[] table;` 3. **使用 `ThreadLocal` 物件去取,代表 ==一個 ThreadLocal 物件可以存多個數值==,而該數值與其它物件無關** ```java= public class DemoTest implements Runnable { ThreadLocal<Thread> tl = new ThreadLocal<>(); @Override public void run() { tl.set(1); // Key : tl, Value : 1 tl.set(2); // Key : tl, Value : 2 tl.set(3); // Key : tl, Value : 3 } } ``` > ![](https://i.imgur.com/tEETvhn.png) ## Appendix & FAQ :::info ::: ###### tags: `Java 基礎進階` `Java 多線程`