併發程式設計基礎(二)—— ThreadLocal及CAS基本原理剖析
本篇博文接上一篇併發程式設計基礎(一)—— 關於java中的執行緒,沒看過的小夥伴請先移步上一篇。本博文相關程式碼已提交至GitHub:https://github.com/ZNKForSky/JavaBasis/tree/master/src/thread。
等待通知機制
執行緒間的通訊依賴於等待通知機制,上篇博文只是對此機制一筆帶過,沒有細講,在這篇博文中作以補充。
機制原理
該機制是指一個執行緒A呼叫了物件O的wait()方法進入等待狀態,而另一個執行緒B呼叫了物件O的notify()或者notifyAll()方法,執行緒A收到通知後從物件O的wait()方法返回,也就是被喚醒了,進而執行後續操作。上述兩個執行緒通過物件O來完成互動,而物件上的wait()和notify/notifyAll()的關係就如同開關訊號一樣,用來完成等待方和通知方之間的互動工作。
PS:wait()、notify()、notifyAll()這些方法都是Object的,而非Thread。
API說明
- wait()
呼叫該方法的執行緒進入 WAITING狀態,直到另一執行緒呼叫notify()/notifyAll()方法通知該執行緒,該執行緒才會被喚醒,之後該執行緒會重新獲取監視器,直到獲取到了監視器的所有權,它才會恢復執行。wait()會釋放物件的鎖,這一點在wait()方法的註釋上也有提到。 - wait(long)
等待的最長時間(以毫秒為單位),也就是等待n毫秒後,如果沒有收到通知就自行喚醒。wait()其實也呼叫的是wait(long)方法,原始碼如下所示:/** * @throws IllegalMonitorStateException 如果當前執行緒不是物件監視器的所有者,也就是說 * 沒有拿到鎖的情況下,呼叫wait()方法就會丟擲此異常 * @throws InterruptedException 如果有任何執行緒在當前執行緒等待通知之前或期間中 * 斷了當前執行緒。 引發此異常時,將清除當前執行緒的中斷狀態。 */ public final void wait() throws InterruptedException { wait(0); }
在呼叫 wait()之前,執行緒必須要獲得該物件的物件級別鎖,即只能在同步方法或同步塊中呼叫 wait()方法,否則會報IllegalMonitorStateException異常:
- wait (long,int)
乍一看,似乎是對於超時時間更細粒度的控制,可以達到納秒級別,細看其實只要指定了合法的大於0的納秒數,毫秒數就會加一。/** * @param timeout 等待的最長時間(以毫秒為單位)。 * @param nanos 額外時間,以納秒為單位,範圍為0-999999。 * @throws InterruptedException 當執行緒處於WAITING或者TIMED_WAITING時, * 對執行緒物件呼叫interrupt()會使得該執行緒丟擲InterruptedException,需 * 要注意的是,丟擲異常後,中斷標誌位會被清空(執行緒的中斷標誌位會由true * 重置為false,因為執行緒為了處理異常已經重新處於就緒狀態),而不是被設定。 */ public final void wait(long timeout, int nanos) throws InterruptedException { if (timeout < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (nanos < 0 || nanos > 999999) { throw new IllegalArgumentException( "nanosecond timeout value out of range"); } if (nanos > 0) { /*只要附加時間的納秒數合法且大於0,就把毫秒數加1*/ timeout++; } wait(timeout); }
- notify()
喚醒正在此物件的監視器上等待的任意一個執行緒,因為可能會有多個執行緒都在爭奪這個鎖,notify()只是喚醒這些執行緒中的其中一個,具有隨機性。拿到物件的鎖的執行緒會從wait()中返回,繼續執行其他業務邏輯,而沒拿到鎖的執行緒重新進入WAITING狀態。 - notifyAll()
通知所有等待在該物件上的執行緒。
等待和通知的標準正規化
等待方遵循如下原則:
- 獲取物件的鎖。
- 如果條件不滿足,那麼呼叫物件的wait()方法,被通知後仍要檢查條件。
- 條件滿足則執行對應的邏輯。
synchronized (obj){ while (<condition does not hold>) obj.wait(); // Perform action appropriate to condition }
通知方遵循如下原則:
- 獲得物件的鎖。
- 改變條件。
- 通知所有等待在物件上的執行緒。
synchronized (obj){ // Change conditions obj.notifyAll(); }
在從 wait()返回前,notifyAll()會喚醒所有處於WAITING和TIMED_WAITING的執行緒,此時所有被喚醒的執行緒會去競爭拿鎖,如果其中一個執行緒獲得了該物件鎖,它就會繼續往下執行,在它退出synchronized程式碼塊,釋放鎖後,其他的已經被喚醒的執行緒將會繼續競爭獲取該鎖,一直進行下去,直到所有被喚醒的執行緒都執行完畢。
PS:儘可能用notifyall(),謹慎使用notify()。
ThreadLocal
ThreadLocal其實很簡單,就是字面意思:執行緒本地變數。也就是說每個執行緒在使用ThreadLocal時都會在當前執行緒中建立一個屬於執行緒本身的本地變數副本,從而實現了執行緒的資料隔離。一句話可能把有些小夥伴搞蒙了,別急,且聽我細細道來。
我們先從ThreadLocal的set(T)方法入手,
/**
* 將此執行緒區域性變數的當前執行緒副本設定為指定值。 大多數子類將不需要重寫此方法,
* 而僅依靠{@link #initialValue}方法來設定執行緒區域性變數的值。
*
* @param value 要儲存在此本地執行緒的當前執行緒副本中的值。
*/
public void set(T value) {
/*獲取呼叫 set方法的執行緒,即當前執行緒*/
Thread t = Thread.currentThread();
/*通過當前執行緒拿到一個叫做 ThreadLocalMap的東西*/
ThreadLocal.ThreadLocalMap map = getMap(t);
/*如果map不為空,則呼叫 ThreadLocalMap的 set(ThreadLocal<?> key, Object value)方法*/
if (map != null)
map.set(this, value);
/*否則建立 ThreadLocalMap例項物件,並把當前執行緒和使用者設定的 value傳進 ThreadLocal的構造中*/
else
createMap(t, value);
}
上面程式碼中提到 ThreadLocalMap,所以接著我們分析 ThreadLocalMap:
如上圖所示,ThreadLocalMap是一個類似於Map的鍵值對資料結構,以ThreadLocal例項物件為鍵,任意物件為值,而它本身是作為 Thread的成員變數附加線上程上,所以 getMap(t)是直接返回Thread的成員。也就是說我們可以根據執行緒的的一個ThreadLocal物件獲取繫結在此執行緒上的一個值。下面通過一個例子讓大家更進一步瞭解它的用法:
package thread.threadlocal;
/**
* @author Luffy
* @Classname UseThreadLocal
* @Description 演示使用 ThreadLocal實現執行緒間資料隔離。
* @Date 2020/12/17 13:52
*/
public class UseThreadLocal {
/**
* 建立一個存放 Integer型別的 ThreadLocal例項並呼叫初始化方法設定執行緒本地變數的初始值為0,
* ThreadLocal實現了執行緒間資料隔離,所以是執行緒安全的。
*/
static ThreadLocal threadLocalInter = new ThreadLocal<Integer>() {
/**
* 返回該執行緒區域性變數的初始值,該方法是一個protected的方法,顯然是為了讓子類覆蓋而設計的。
* 這個方法是一個延遲呼叫方法,線上程第1次呼叫get()或set(Object)時才執行,並且僅執行1次。
* ThreadLocal中的預設實現直接返回一個null。
* @return 初始值
*/
@Override
protected Integer initialValue() {
return 1;
}
};
public static void main(String[] args) throws InterruptedException {
/*開啟三個執行緒*/
for (int i = 0; i < 3; i++) {
new Thread(new WorkThread(i)).start();
/*加休眠錯開執行緒開啟時間*/
Thread.sleep(10);
}
/**
* 將當前執行緒區域性變數的值刪除,目的是為了減少記憶體的佔用,該方法是JDK 5.0新增的方法。需要指出的是,
* 當執行緒結束後,對應該執行緒的區域性變數將自動被垃圾回收,所以顯式呼叫該方法清除執行緒的區域性變數並不是
* 必須的操作,但它可以加快記憶體回收的速度。
*/
threadLocalInter.remove();
}
/**
* 測試執行緒:執行緒的工作是將ThreadLocal變數的值變化,並寫回,看看執行緒之間是否會互相影響
*/
static class WorkThread implements Runnable {
int id;
public WorkThread(int id) {
this.id = id;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":start");
/*該方法返回當前執行緒所對應的執行緒區域性變數。*/
int oldValue = (int) threadLocalInter.get();
/*設定當前執行緒的執行緒區域性變數的值。*/
threadLocalInter.set(id + oldValue);
System.out.println("執行緒" + Thread.currentThread().getName() + "中本地變數新值是: " + threadLocalInter.get());
}
}
}
CAS
CAS基本原理
CAS是Compare And Swap的縮寫,意為比較並且交換,是一個原子操作。下面是百度百科關於“原子操作”的定義:
如果這個操作所處的層(layer)的更高層不能發現其內部實現與結構,那麼這個操作是一個原子(atomic)操作。
原子操作可以是一個步驟,也可以是多個操作步驟,但是其順序不可以被打亂,也不可以被切割而只執行其中的一部分。
將整個操作視作一個整體是原子性的核心特徵。
通俗的講,就是一個操作無論它內部多麼複雜,但對於外界看來只是一個操作,它要麼執行,要麼不執行。比如我打了小明一巴掌,這個操作就可以視為原子操作,我要麼打了要麼沒打,不能存在我舉起手來然後又放下去的情況,要麼出手就打小明一巴掌,要麼就別出手。
CAS的基本思路就是,如果這個地址上的值和期望的值相等,則給其賦予新值,否則重新獲取地址上的值,再和期望的值作比較,如此往復,直至成功。如下圖所示:
CAS實現原子操作的三大問題
ABA問題
因為CAS需要在操作值的時候,檢查值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。
舉個例子:現有一個用單向連結串列實現的堆疊,棧頂為A,這時執行緒T1已經知道A.next為B,然後希望用CAS將棧頂替換為B: head.compareAndSet(A,B)。
在T1執行上面這條指令之前,執行緒T2介入,將A、B出棧,再pushD、C、A,此時堆疊結構如下圖,而物件B此時處於遊離狀態:
此時輪到執行緒T1執行CAS操作,檢測發現棧頂仍為A,所以CAS成功,棧頂變為B,但實際上B.next為null,所以此時的情況變為:
其中堆疊中只有B一個元素,C和D組成的連結串列不再存在於堆疊中,平白無故就把C、D丟掉了。
上述例子來源:CAS原理分析
ABA問題的解決思路就是使用版本號。在變數前面追加上版本號,每次變數更新的時候把版本號加1,那麼A→B→A就會變成1A→2B→3A。舉個通俗點的例子,你從飲水機接了杯開水,水溫太高需要涼一涼,於是你去幹別的事了,然後你鄰桌的同事小趙剛出差回來渴的不行,就把你涼的水喝掉了,其他同事提醒小趙那個水是你涼的,於是小趙趕緊重新倒了一杯水,你回來看水還在,拿起來就喝,如果你不管水中間被人喝過,只關心水還在,這就是ABA問題。
如果你是一個有潔癖的小夥子,不但關心水在不在,還要在乎你離開的時候水被人動過沒有,於是你買了一個可以在開啟蓋子之後計數的水杯,只要在你離開後別人開啟過水杯,就說明水杯被別人動過。
JDK1.5之後,JUC.atomic包下提供了AtomicMarkableReference和AtomicStampedReference解決ABA問題,它倆的唯一區別就是AtomicMarkableReference只關心引用變數是否被更改過,AtomicStampedReference除此之外還可以知道引用變數被更改過幾次。
/**
* 使用給定的初始值建立一個新的{@code AtomicMarkableReference}。
*
* @param initialRef 初始引用
* @param initialMark 初始標記,用來標記引用變數是否被更改過
*/
public AtomicMarkableReference(V initialRef, boolean initialMark) {
pair = AtomicMarkableReference.Pair.of(initialRef, initialMark);
}
/**
* 使用給定的初始值建立一個新的{@code AtomicStampedReference}
*
* @param initialRef 初始引用e
* @param initialStamp 初始戳
*/
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = AtomicStampedReference.Pair.of(initialRef, initialStamp);
}
通過它倆的構造方法,我們可以看到AtomicMarkableReference使用boolean變數mark標記引用變數是否被更改過,而AtomicStampedReference使用int變數stamp標記引用變數被更改過幾次。
解決ABA問題的示例:
package thread.cas;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* @author Luffy
* @Classname UseAtomicStampedReference
* @Description 使用JDK提供的 AtomicStampedReference解決CAS的ABA問題。
* @Date 2020/12/19 23:44
*/
public class UseAtomicStampedReference {
static AtomicStampedReference<String> asr
= new AtomicStampedReference("Luffy", 0);
public static void main(String[] args) throws InterruptedException {
/*拿到當前的版本號(舊)*/
final int oldStamp = asr.getStamp();
/*拿到當前的引用變數(舊)*/
final String oldReference = asr.getReference();
System.out.println(Thread.currentThread().getName() + "當前變數值: " + oldReference + ",當前版本戳: " + oldStamp);
Thread rightStampThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":當前變數值:"
+ oldReference + ",當前版本戳:" + oldStamp + ","
+ asr.compareAndSet(oldReference,
oldReference + " Java", oldStamp,
oldStamp + 1));
}
});
Thread errorStampThread = new Thread(new Runnable() {
@Override
public void run() {
String reference = asr.getReference();
System.out.println(Thread.currentThread().getName()
+ ":當前變數值:"
+ reference + ",當前版本戳:" + asr.getStamp() + ","
+ asr.compareAndSet(reference,
reference + " C", oldStamp,
oldStamp + 1));
}
});
rightStampThread.start();
rightStampThread.join();
errorStampThread.start();
errorStampThread.join();
System.out.println(asr.getReference() + "============" + asr.getStamp());
}
}
迴圈時間長開銷大
自旋CAS指令如果長時間不成功,會給CPU帶來非常大的執行開銷。
只能保證一個共享變數的原子操作
當對一個共享變數執行操作時,我們可以使用迴圈CAS的方式來保證原子操作,但是對多個共享變數操作時,迴圈CAS就無法保證操作的原子性,這個時候就可以用鎖。
還有一個取巧的辦法,就是把多個共享變數合併成一個共享變數來操作。比如,有兩個共享變數i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。從Java 1.5開始,JDK提供了AtomicReference類來保證引用物件之間的原子性,就可以把多個變數放在一個物件裡來進行CAS操作。示例程式碼如下:
package thread.cas;
import java.util.concurrent.atomic.AtomicReference;
/**
* @author Luffy
* @Classname UseAtomicReference
* @Description 使用JDK提供的 AtomicReference解決“CAS只能保證一個共享變數的原子操作”的問題
* @Date 2020/12/19 23:37
*/
public class UseAtomicReference {
static AtomicReference<UserInfo> atomicUserRef;
public static void main(String[] args) {
/*要修改的實體的例項*/
UserInfo user = new UserInfo("Luffy", 17);
atomicUserRef = new AtomicReference(user);
UserInfo updateUser = new UserInfo("Lady Gaga", 25);
atomicUserRef.compareAndSet(user, updateUser);
System.out.println(atomicUserRef.get());
System.out.println(user);
}
/**
* 定義一個使用者實體類
*/
static class UserInfo {
private volatile String name;
private int age;
public UserInfo(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "UserInfo{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
}
這一講先講這麼多,欲知後事如何,且聽下回分說~歡迎大家評論加點贊,大夥的支援對我很重要哦(≧◉◡◉≦)
相關文章
- Java併發程式設計:深入剖析ThreadLocalJava程式設計thread
- 併發程式設計之 ThreadLocal 原始碼剖析程式設計thread原始碼
- 併發程式設計——基礎概念(二)程式設計
- Java併發程式設計-CASJava程式設計
- Java併發程式設計—ThreadLocalJava程式設計thread
- Java併發程式設計 -- ThreadLocalJava程式設計thread
- Java併發程式設計——ThreadLocalJava程式設計thread
- 併發程式設計之:ThreadLocal程式設計thread
- Java併發程式設計——基礎知識(二)Java程式設計
- Golang併發程式設計基礎Golang程式設計
- 併發程式設計基礎(下)程式設計
- 併發程式設計基礎(上)程式設計
- Java併發程式設計基礎Java程式設計
- 併發程式設計之 CAS 的原理程式設計
- 併發程式設計 — CAS 原理詳解程式設計
- 併發程式設計基礎底層原理學習(二)程式設計
- 併發程式設計——基礎概念(一)程式設計
- Java併發程式設計之Java CAS操作Java程式設計
- Go併發程式設計之美-CAS操作Go程式設計
- 譯文《Java併發程式設計之CAS》Java程式設計
- 簡單學:併發程式設計之 ThreadLocal程式設計thread
- 併發程式設計基礎與原子操作程式設計
- 併發程式設計基礎——JMM簡介程式設計
- Go 併發程式設計 - Goroutine 基礎 (一)Go程式設計
- 併發程式設計(二)程式設計
- 併發程式設計之多執行緒基礎程式設計執行緒
- Java併發程式設計-執行緒基礎Java程式設計執行緒
- Java併發程式設計——基礎知識(一)Java程式設計
- Go 併發程式設計 - 併發安全(二)Go程式設計
- 併發程式設計之 wait notify 方法剖析程式設計AI
- 併發程式設計之 LinkedBolckingQueue 原始碼剖析程式設計原始碼
- 併發程式設計之 ConcurrentLinkedQueue 原始碼剖析程式設計原始碼
- 鴻蒙程式設計江湖:併發程式設計基礎與鴻蒙中的任務併發鴻蒙程式設計
- shell程式設計基礎二程式設計
- 併發程式設計(二)——併發類容器ConcurrentMap程式設計
- [併發程式設計]-關於 CAS 的幾個問題程式設計
- Java併發程式設計-鎖及併發容器Java程式設計
- 併發程式設計之ThreadLocal、Volatile、synchronized、Atomic關鍵字程式設計threadsynchronized