---
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` 方法但並沒有加上同步,就沒有同步功能
>
> 
:::
### 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 訪問同一個物件的方法,會依照順序訪問 !
>
> 
### 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` 鎖的是一個物件
> 
:::
**--實作--**
> 
:::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 互搶!因為這行程式沒有被同步!!
> 
:::
### 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` 訪問不同物件
* **同步 `靜態方法`**:由於是靜態方法,所有是所有物件共用的方法,所以不同物件也仍會做等待的行為
> 
* **同步 `靜態物件`**:其實仍相同,由於使用 static 物件同步,所以可以達到相同的效果
> 
* **同步 `Class` 類**:同上,由於在 JVM 內一個物件只會有一個 Class 物件,所以可以等同於同步靜態物件
> 
## 多執行序之間協做
一個任務可以交給多個 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 相同)
> 
:::warning
* **使用 `wait()` 要搭配 `while()` 判斷式**
使用 if,可以看出 **notify 後會從 wait 休眠的行數 繼續執行**
> 
:::
### 鎖的釋放時機
* 鎖的釋放時機:一般來說有以下幾個時機會釋放鎖
* 執行完同步程式碼區塊
* **出現例外狀況(拋出異常)導致執行序被終止,鎖就會自動釋放**(這很重要,避免程序造成死鎖無法正常運行)
* **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();
}
}
}
```
**--實做--**
> 
### 認識公平鎖 & 非公平鎖
* 公平鎖就是依照時間去排序,去執行 (**公平鎖的獲取是順序的**)
* 非公平鎖的的獲取是「**搶順序**」
* 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");
}
}
}
}
}
```
> 
:::info
* 如果我們把鎖的順序對調,也就是將兩個執行序取鎖的順序調整為一樣順序,那就可以解開這個死鎖
> 
:::
* **活鎖**:代表 **不斷的嘗試拿取鎖,並且當無法獲得鎖時就釋放鎖**,範例如下…
> 採用嘗試拿鎖的機制,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");
}
}
}
}
}
```
**--範例--**
> 
### 樂觀鎖 & 悲觀鎖
* **悲觀鎖**:`synchronized` 即為悲觀鎖,**不管有沒有搶資源都依率鎖住,這樣會導致效能下降** (好像總有人要跟它搶鎖,不管 3721 先鎖住資源就對了)
* **樂觀鎖**:像是 CAS 就是樂觀鎖 (下面會在介紹),**複製一份副本,先進行作業,如果比對後發現不同於原來副本,則再次複製,一直循環到成功為止**
:::success
* **以效率來說是 ++樂觀鎖的效率會比較高++**
因為當執行序休眠需要 `10000` ~ `20000` 個指令時間,而 cpu 假設執行一個指令需要 `0.6u` 時間,那休眠 + 喚醒一次所花的時間,也就是 `20000 * 0.6u * 2 = 24ms`,**當執行一次作業不需要這麼長時間時就是樂觀鎖效率較高**
:::
## CAS 原子操作
全名是 `Compare And Swap`(比較和交換),這是 **使用 CPU 的特殊指令集,它會確保該操作是連貫動作「不可再切割」**(有就是動作的最小單位)
**使用 CAS 操作時,每個執行序都會取得目標值的「副本」,在進行操作後會先比較原來的值是否為當初複製的值,如果不是則重新複製,重新操作**(樂觀鎖)
> 
:::info
效率由低至高:**`Synchronized` < `ReentrantLock` < `CAS`**
:::
### 注意 CAS 的問題
* CAS 主要要注意三個問題,在考量這三個問題後再決定是否要用 CAS、或是直接改為同步
1. **ABA 問題** : CAS 會在放入數值後比對,如果數值如同剛複製的值就設定,否則就再次取值,要考慮到中途可能已經被修改過,然後再次改過來
:::info
簡單來說就是中間被修改過也不會知道
:::
> 複製值 A,執行操作到 C,放回去比對,也比對為 A 可設定 (但是必須考量到,比對的 A 可能中間已經被修改過了 A -> B -> A)
>
> 
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,返回新值
}
}
}
```
**--實作結果--**
> 
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;
}
}
}
```
**--實作結果--**
> 
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());
}
}
```
**--實作結果--**
> 
## 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`** 來操作這個變數(這樣就是安全的操作)
:::
**--實做--**
> 
### 手做 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());
}
}
}
```
**--實做--**
> 
### ThreadLocal 源碼分析
* 從 ThreadLocal 開始分析,會發現 ThreadLocal & Thread 有關係,**每一個 Thread 中都會儲存一個 ThreadLocalMap 物件**
> 
以下我們來看看 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
}
}
```
> 
## Appendix & FAQ
:::info
:::
###### tags: `Java 基礎進階` `Java 多線程`