Overview of Content
在這篇全面解析多執行緒與同步技術的文章中,我們將深入探討多執行緒的安全性,從基本的執行緒安全考量點到 Java synchronized
的詳細用法,包括方法、同步區塊、靜態方法和靜態物件的同步
我們還會探討多執行緒之間的協作機制,如 wait
方法與 notify
/ notifyAll
,以及鎖的釋放時機。此外,我們將詳細介紹 ReentrantLock
機制,包括可重入鎖、顯式鎖與隱式鎖、公平鎖與非公平鎖,並解析死鎖與活鎖的概念,樂觀鎖與悲觀鎖的對比
文章還涵蓋了 CAS
原子操作及其在 JDK 中的應用,並通過簡單範例、手動實現及源碼分析,讓讀者全面了解 ThreadLocal 如何實現執行緒隔離。這篇文章將為讀者提供深入的多執行緒與同步技術知識,幫助解決實際開發中的各種挑戰
以下可能會混用 “線程”、“執行序”,兩者是相同意思
寫文章分享不易,如有引用參考請詳註出處,如有指導、意見歡迎留言(如果覺得寫得好也請給我一些支持),感謝 😀
個人程式分享時比較注重「縮排」,所以可能不適合手機的排版閱讀,建議切換至「電腦版」、「平板版」視窗看
多執行序:安全概述
多執行序是常見可以加強 CPU 吞吐量的手段,可以有效提高效率(正確使用的話),但由於 多執行緒是程式設計提供的概念(並且每種語言的實現並不同),它不具備保證資源存儲的安全性!
多執行序的不安全是在 對同個物件進行寫入 (改變) 資源時(讀取則不會有安全問題)
● 這裡談及的是「執行序,
Thread
」而不是程序程序(也就是「應用」)是核心系統提供的功能,核心系統會將其稱之為
Process
,它就具備資源訪問安全性,它讓每個應用都活在虛擬記憶體中不會相互干擾
執行緒安全:考量點
● 多執行序安全往往是以「性能」作為代價(鎖),因此我們使用鎖 🔒 需考慮到以下幾點
● 只對可能產生資源搶奪的區塊進行代碼同步,同步的代碼越長效能越差,因為要做更多的等待
如果沒有安排的隨意使用執行緒,那代碼的執行效率反而會不升反降
● 我們也可以在創建類時區分 「單執行緒執行環境」、「多執行緒執行環境」,避免統一用多執行環境的物件(這是一種開發上的約束)
Java synchronized 同步
● synchronized
是 Java 語言在使用同步時的一個「修飾符」,一次只讓一個執行序訪問,一個物件讓多個執行序訪問;以下是幾個 Java synchronized
修飾符使用時要注意的事情…
● 可以
Synchronized
Null 物件 ? 不行 !Synchronized 一定是針對某一物件做出同步動作,所以一定要有物件(要有物件可以搶,不然就無法同步了)
●
synchronized
效果可以繼承 ? 不行 !Override 不可繼承 synchronized 效果,繼承時 要自己加入
synchronized
關鍵字,但是可以透過 super(呼叫父類的同步方法)以下案例中來證明 synchronized 不會作用在繼承
TestSynch
子類覆寫addData
方法但並沒有加上同步,就沒有同步功能
synchronized 方法:自動鎖定
● 這裡的自動鎖動,是指使用 synchronized
時不去指定物件,而使用預設的 this
物件
認識 this
:每一個 物件都有內含 this
關鍵字,它代表的是這個物件的 instance
實例,它是個隱藏物件
● 接下來我們 使用 synchronized
關鍵字來同步方法,就是手法鎖定 this
物件,鎖的物件是目前的實例(也就是 this
);接下來凡事使用到該物件的地方都需要做等待
當然,請特別注意它鎖定的是「實例」,也就是 多個執行序都要訪問同一個實例,那才有所定的功能;如果多個執行序訪問不同的實例,那就沒有同步的效果
// 同步方法
synchronized void addData() {
// TODO:
}
// 區塊同步,鎖的物件同上,下一小節會說明…
void addData() {
synchronized(this) {
// TODO:
}
}
--實作--
上面的範例 15 個太多 (不方便觀察),改為 5 個;可以看到使用兩個 Thread 訪問同一個物件的方法,會依照順序訪問 !
synchronized 同步區塊:指定同步物件
● synchronized
除了上述的 this
物件可以鎖定之外,也允許我們指定一個物件來鎖定(也可以是 this
,因為自身就是一個物件),持有此物件的 Thread 才可執行同步的函數
● 透過指定物件而不使用
this
的好處在於,可以更精細的去調整同步,可以有效的提高效率也就是 可以在一個物件中使用多個「物件(也就是鎖)」,而不是全部執行序都搶
this
這把鎖
● synchronized
指定 this
物件來鎖定,效果跟「synchronized 方法」是一樣的
public void addData() {
synchronized(this) {
//TODO:
}
}
}
● synchronized
物件指定自己創建物件來鎖定
private Object o = new Object(); // 一定是實例化,不實例化無法作為 key
// or
// private byte[] b = new byte[0]; // byte[] array 比 Object 需要更小的範圍
public void addData() {
synchronized(this) {
//TODO:
}
}
}
● 但這個 物件(鎖)一定要實例化,不可以為 null,否則會報錯,因為
synchronized
鎖的是一個物件
--實作--
● 接下來的程式,我們再次強調「鎖定區塊」的特性:
看看區塊同步鎖,是否只同步區塊內部(鎖定的區域),而非區塊內的程式不同不
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 知識請點連結
● JVM 內存模型中,靜態方法、物件物件、類 (
Class
) 都是存在不同地方
目標 內存模型儲存位置 JVM 中物件數量是否單一 靜態方法 方法區 Y 靜態物件 靜態變量區 Y 物件( instance
)堆區 N
// 同步靜態方法
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 狀態 |
● 注意:喚醒是透過物件(鎖)來喚醒,並且喚醒時(
notify
/notifyAll
)是要同一個鎖的物件來喚醒,不同物件(鎖)不能被通知!
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");
}
}
}
A. 使用 run()
會變成排隊執行(main Thread wait work Thread
),導致程式無法正常運作
B. 必須持有「同一把物件(鎖)」,如果持有不同鎖則無法正常喚醒目標物件;this
是指 TestClass 物件鎖(並非類鎖)
C. Java 的每一個物件都可以作為鎖 (Object)
D. wait()
內如果設置時間 (單位預設為ms),就是限制等待時間,等待時間一過就自動 喚醒;如下圖可以看到,1s 喚醒一次,但 wait 只休息 500ms,所以在 while 內喚醒了 2 次,所以判斷了兩次 (time 相同)
● 使用
wait()
要搭配while()
判斷式使用 if,可以看出 notify 後會從 wait 休眠的行數 繼續執行
鎖的釋放時機
● 鎖的釋放時機:一般來說有以下幾個時機會釋放鎖
● 執行完同步程式碼區塊
● 出現例外狀況(拋出異常)導致執行序被終止,鎖就會自動釋放(這很重要,避免程序造成死鎖無法正常運行)
● Object#wait
方法:這個方法很特別,它會將當前執行緒掛起(Blocking),並且釋放鎖,讓其他執行序執行該方法!
● Thread 的 sleep 方法 & Object 的 wait 方法兩者個區別
比較點 Thread#sleep Object#wait 釋放 CPU 資源 Y Y 重入時,是否釋放鎖(重入鎖下個小節說明) 不釋放 釋放鎖 是否需要喚醒 不需要,只會睡眠規定時間 可以持續等待直到被喚醒 區域性 無 必須在 synchronized
區塊內使用
ReentrantLock 機制:認識更多不同的鎖
在 Java 中,ReentrantLock 是一種提供 可重入鎖(reentrant lock
)功能的鎖實現,屬於 java.util.concurrent.locks
包
ReentrantLock 提供了比 synchronized
關鍵字更靈活的鎖機制,允許更細粒度的鎖控制,並提供了一些額外的功能
● ReentrantLock 的主要特性
A. 可重入性:如果一個執行序已經獲得了鎖,可以再次獲得鎖而不會被阻塞(synchronized
也同樣是可重入)
B. 公平鎖:ReentrantLock 可以配置為公平鎖,確保等待時間最長的執行序最先獲得鎖
默認情況下是非公平鎖,因為非公平鎖的效能較加
C. 可中斷鎖:等待鎖的執行序可以被中斷
D. 嘗試獲取鎖:可以嘗試在獲取鎖時設置超時,防止長時間等待
E. 提供條件變量:ReentrantLock 可以產生多個 Condition 對象,實現更複雜的執行序間同步
認識可重入鎖
● 鎖是否可重入的行為在「遞歸」的程式設計中相當重要;在「不可重入鎖」中使用遞歸會造成程式卡死… 而在「可重入鎖」遞歸調用時,可以重新獲得鎖,再次進入執行程式
遞歸的程式設計概念如下
// 概念程式
synchronized int showData(int x) {
if(x <= 0) {
return 0;
}
return 1 + showData(x - 1); // 遞歸呼叫,重入同步方法
}
● synchronized 默認可支持重入鎖,如果非重入鎖,就會發生死鎖,因為一直在等待開鎖,使用 遞歸就可證明 synchronized 是重入鎖
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 虛擬機自動處理的
public synchronized void someMethod() {
// 此方法由隱式鎖保護
}
public void someMethod() {
synchronized (this) {
// 這段代碼由隱式鎖保護
}
}
當執行序進入 synchronized
區塊或方法時,自動獲取鎖;當執行序離開 synchronized
區塊或方法時,自動釋放鎖
● 隱式鎖:ReentrantLock 是顯式鎖,因為它的加鎖、解鎖都要開發者自己來操作,所以 把解鎖放在 finally 中很重要
● 明確的加鎖和解鎖:使用 ReentrantLock 時,需要明確地調用 lock() 方法來獲取鎖,並在不需要鎖時調用 unlock() 方法來釋放鎖…
private final ReentrantLock lock = new ReentrantLock();
public void someMethod() {
lock.lock();
try {
// 這段代碼由顯式鎖保護
} finally {
lock.unlock();
}
}
在 try 區塊內進行需要同步的操作,並在 finally 區塊中確保鎖被釋放,即使在出現異常的情況下也能保證鎖被釋放,避免死鎖
● 高控制度:ReentrantLock 提供了更多功能和更高的控制度,比如可中斷鎖、嘗試獲取鎖以及公平鎖的支持,使得其在需要更複雜同步機制的情況下更具優勢
● 條件變量:ReentrantLock 提供了 Condition 物件,用於實現更複雜的執行序間協調
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 條件控制
● ReentrantLock 也可以達到跟 synchronized 相同的同步效果,而要保障的安全同步操作區需要在獲取鎖(lock
)、釋放鎖(unlock
)之間執行
● 另外說到喚醒,ReentrantLock 自身就可以喚醒鎖… 而且 ReentrantLock 也提供條件控制類 Condition
來達到更細節的控制
ReentrantLock 可以取得多個 Condition 物件,未來可以用在 不同的條件喚醒,使用 一個 condition 對應一個鎖的條件可達到更精準的控制(對於效能也有一定的改善)
Conditoin 的操作必須在 lock & unlock 之間
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 獲得公平鎖
Lock lock = new ReentrantLock(true); // 設置為公平鎖
● 非公平鎖的效率較高,為甚麽?
假設喚醒執行序要 1000us 休眠需要 1000us,現在有 A、B、C 執行序,A 搶到鎖(假設鎖的流程 A-B-C),B 休眠 3s,C 休眠 1s
A. 依照公平鎖,執行順序就是 A-B-C,C 醒後 1000us 會在休眠 1000us,這樣 C 就要喚醒在睡眠花 2000us (因為要等 B 結束)
B. 如果是非公平鎖,執行順序就是先醒 C 的可先搶到資源,並在B 醒之前釋放鎖,這樣效率更高
認識死鎖 & 活鎖
● 死鎖指的是,兩個以上的執行序(m >= 2
), 搶奪兩個以上的資源(n >= 2
),並且死鎖在學術上也有 4 個定義,如下所述
A. 請求 & 持有 : 當一個執行序持有一個資源後將其保持,並開始請求下一個資源
B. 互斥 : 當一個資源只能有一個執行序持有,當持有後就會鎖住不讓其他執行序獲取該資源
C. 不剝奪 : 當一個執行序獲得資源後,在為完成任務之前是不能被剝奪權力的,只能透過該資源自己釋放
D. 環狀等待 : A 等待 B 釋放資源,但 B 又在等 A 釋放資源
而這些死鎖上的定義也有相對的處理方式
狀態 | 處理方式 |
---|---|
請求 & 持有 | 執行序運行前先獲取所有資源,沒有獲得所需全部資源則不運行 |
互斥 | 修改獨佔資源改為虛擬資源,類似於指標物件,但大部分已無法修改 |
不剝奪 | 當一個執行序持有一個資源後,在去獲取另一個物件失敗時,就連之前的資源一起釋放 |
環狀等待 | 所有進程只能按照編號申請資源 |
● 解決死鎖的關鍵在,拿鎖的順序一致,以下我們來試試看死鎖、解除死鎖的方式
從下面的範例可以看它們 取鎖的順序是相反不同的(兩個執行序對 Lock_1
、Lock_2
兩個鎖的順序不同),所以會導致死鎖,如果順序相同就不會產生死鎖 (如果改為相同順序就可以正常取鎖)
// 死鎖範例
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");
}
}
}
}
}
● 如果我們把鎖的順序對調,也就是將兩個執行序取鎖的順序調整為一樣順序,那就可以解開這個死鎖
● 活鎖:代表 不斷的嘗試拿取鎖,並且當無法獲得鎖時就釋放鎖,範例如下…
// 活鎖範例
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");
}
}
}
}
}
採用嘗試拿鎖的機制,Lock's tryLock 方法等等
--範例--
樂觀鎖 & 悲觀鎖
● 悲觀鎖:synchronized
即為悲觀鎖,不管有沒有搶資源都依率鎖住,這樣會導致效能下降 (好像總有人要跟它搶鎖,不管 3721 先鎖住資源就對了)
● 樂觀鎖:像是 CAS 就是樂觀鎖 (下面會在介紹),複製一份副本,先進行作業,如果比對後發現不同於原來副本,則再次複製,一直循環到成功為止
● 以效率來說是 樂觀鎖的效率會比較高
因為當執行序休眠需要
10000
~20000
個指令時間,而 cpu 假設執行一個指令需要0.6u
時間,那休眠 + 喚醒一次所花的時間,也就是20000 * 0.6u * 2 = 24ms
,當執行一次作業不需要這麼長時間時就是樂觀鎖效率較高
CAS 原子操作
全名是 Compare And Swap
(比較和交換),這是 使用 CPU 的特殊指令集,它會確保該操作是連貫動作「不可再切割」(有就是動作的最小單位)
使用 CAS 操作時,每個執行序都會取得目標值的「副本」,在進行操作後會先比較原來的值是否為當初複製的值,如果不是則重新複製,重新操作(樂觀鎖)
效率由低至高:
Synchronized
<ReentrantLock
<CAS
注意 CAS 的問題
● CAS 主要要注意三個問題,在考量這三個問題後再決定是否要用 CAS、或是直接改為同步
A. ABA 問題 : CAS 會在放入數值後比對,如果數值如同剛複製的值就設定,否則就再次取值,要考慮到中途可能已經被修改過,然後再次改過來
簡單來說就是中間被修改過也不會知道
複製值 A,執行操作到 C,放回去比對,也比對為 A 可設定 (但是必須考量到,比對的 A 可能中間已經被修改過了 A -> B -> A)
B. 循環時間開銷大 : 如果一直設定失敗會造成 CPU 負擔,並且花費時間也長,需要衡量
C. 只能保證原子操作 : 如果操作 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,只關心是否有被動過 | - |
A. AtomicInteger 的使用範例
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,返回新值
}
}
}
--實作結果--
B. AtomicReference 的使用範例
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;
}
}
}
--實作結果--
C. AtomicStampedReference 的使用範例
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
為自己設定的數值,可以用來隔離共享變數的操作,讓每個執行序都擁有一個自身變量的操作,不會相互影響
●
ThreadLocal
本身並不是用來保障同步行為的!它的主要目的是為每個執行序提供獨立的變量副本,從而避免多執行序環境下的變量共享問題… 這種設計可以避免執行序之間的數據競爭,從而簡化多執行序編程中的數據隔離問題
ThreadLocal 簡單範例
● 以下我們就來在多執行序的狀況下,透過 ThreadLocal 隔離變量,讓每個執行序自身安全的操作一個共享變量(正確點來說是共享變量的副本)
// 使用 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 變量而不影響其他執行序
● ThreadLocal 簡單來說就是把共享的變數,拷貝原型後後制到目前所在的執行序中,讓每一個 Thread 都擁有此變數,而該變數互不相干
ThreadLocal 的內部實做是 將靜態變數儲存到
ThreadLocalMap
普通變數,並存於每個 Thread 中,每一個 Thread 都訪問自己的ThreadLocalMap
來操作這個變數(這樣就是安全的操作)
--實做--
手做 ThreadLocal 機制
● 自己 實踐一個與 ThreadLocal 相同效果的類,它針對每一個執行序給予執行序自身的變量,來達到相同的安全操作(請注意,這邊不是指安全的同步,而是指安全的隔離操作變量)
其中的重點有兩個
A. 透過 Map 把執行序作為 Key
來保存,並且在設置、取值的時候都透過 Thread.currentThread()
來取出當前的執行序
B. 需要一把鎖,來鎖定對於 set
、get
的操作,來保證多執行序對於 Map 的同步操作(以下用自己創建鎖的方式)
// 手做 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 操作
// 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);
}
A. 從這裡可以看得出來它是使用 currentThread 當作 key,但是 ThreadLocal 並不是使用 Map 來儲存,而是 TheadLocalMap 這個物件
B. 每一個 Thread 物件中都有 ThreadLocalMap 屬性,該物件也不是靜態物件,當該物件為空時就在內部創建 ThreadLocalMap,而 ThreadLocalMap 內部有一個屬性 Entry[]
數組
private Entry[] table;
C. 使用 ThreadLocal
物件去取,代表 一個 ThreadLocal 物件可以存多個數值,而該數值與其它物件無關
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
}
}
更多的 Java 語言相關文章
Java 語言深入
● 在這個系列中,我們全方位地探討了 Java 語言的各個核心主題,旨在幫助你徹底掌握這門強大的編程語言。無論你是想深入理解 Java 的基礎類型與變數作用域,還是探索異常處理與運算子的細節,這些文章都將為您提供寶貴的知識
此外,我們還涵蓋了物件創建、函數式編程、註解應用以及泛型的深入分析,幫助您提升在實際開發中的技能和效率
點擊以下連結,開始你的學習之旅~
● 深入探索 Java 基礎類型、編碼、浮點數、參考類型和變數作用域 | 探討細節
● 深入了解 Java 應用與編譯:從原始檔到命令產出 JavaDoc 文件 | JDK 結構
● 深入理解 Java 運算子與修飾符 | 重要概念、細節 | equals 比較
● 深入探索 Java 物件創建與引用細節:Clone 和finalize 特性,以及強、軟、弱、虛引用
● 認識 Java 函數式編程:從 Lambda 表達式到方法引用 | 3 種方法引用
Java IO 相關文章
● 探索 Java IO 的奧秘,了解檔案操作、流處理、NIO等精彩內容!
深入 Java 多執行緒
● 這一系列文章將帶你深入了解 Java 多執行緒技術的各個方面,從基礎知識到進階應用,涵蓋了多執行緒編程的核心概念與實踐
深入 Java 物件導向
● 探索 Java 物件導向的奧妙,掌握介面、抽象類、繼承等重要概念!幫助你針對物件導向設計有更深入的了解!