
上一節你應該學習了thread的基本知識和原始碼原理,熟悉了執行緒的應用場景,這一節來學習下和Thread相關的一個類,ThreadLocal,

什么是ThreadLocal?
什么是ThreadLocal?
字面意思是執行緒本地變數的意思,用一句話解釋就是:執行緒本地的變數副本,屬于每個執行緒自己獨有的,
為什么說是變數副本呢?因為每個執行緒使用ThreadLocal設定自己的值,設定的值互相之間不受影響,但是使用的是同一個ThreadLocal物件,所以設定的每個變數,是給每個執行緒一個獨有的變數副本,
你可以畫一個圖來理解下:

當你知道了什么是ThreadLocal后,讓我們簡單來使用一下它,看下他的使用效果,
Hello ThreadLocal
Hello ThreadLocal
下面通過一段Hello ThreadLocal小程式,讓你回顧下ThreadLocal的使用,假設有這么一個場景:
執行緒啟動了2個執行緒,使用threadLocal設定了一個Loan物件,main執行緒也設定了自己的loan物件,執行緒2和main執行緒在設定前嘗試訪問threadLocal中的資料,
代碼實作如下:
public class HelloThreadLocal {
private static ThreadLocal<Loan> threadLocal = new ThreadLocal<Loan>();
public static void main(String[] args) {
//執行緒1 使用threadLocal設定自己的變數副本
new Thread(() -> {
threadLocal.set(new Loan("zhangsan", "1000.00"));
System.out.println("執行緒-1loan:"+threadLocal.get());
}).start();
//執行緒2 使用threadLocal設定自己的變數副本
new Thread(() -> {
try {
Thread.sleep(1 * 1000);
} catch (InterruptedException e) {
}
HelloThreadLocal.Loan loan = threadLocal.get();
System.out.println("執行緒-2loan:"+loan);
threadLocal.set(new Loan("lisi", "2000.00"));
loan = threadLocal.get();
System.out.println("執行緒-2loan:"+loan);
}).start();
try {
Thread.sleep(5 * 1000);
} catch (InterruptedException e) {
}
System.out.println("main-執行緒loan:"+threadLocal.get());
threadLocal.set(new Loan("wangwu", "1000.00"));
System.out.println("main-執行緒loan:"+threadLocal.get());
}
@Data
@AllArgsConstructor
public static class Loan {
private String name;
private String amount;
}
}
上面的代碼輸出結果如下:
執行緒-1loan:HelloThreadLocal.Loan(name=zhangsan, amount=1000.00)
執行緒-2loan:null
執行緒-2loan:HelloThreadLocal.Loan(name=lisi, amount=2000.00)
main-執行緒loan:null
main-執行緒loan:HelloThreadLocal.Loan(name=wangwu, amount=1000.00)
可以看出,每個執行緒無法獲取到其他執行緒設定的loan物件,哪怕是使用同一個ThreadLocal設定的,為什么會這樣呢?其實就是因為每個執行緒的變數副本,ThreadLocal只是一個工具,操作了執行緒本地的變數副本而已,具體原理如下圖所示:

ThreadLocal原始碼剖析get方法脈絡
ThreadLocal原始碼剖析get方法脈絡
相信你通過上面的例子,已經理解ThreadLocal的作用了,它的底層是如何做到的呢?你需要分析一下它的原始碼了,
這里我們把栗子簡化下,可以更好的分析get、set方法的原始碼,簡化HelloThreadLocal代碼如下:
public class ThreadLocalGetMethod {
private static ThreadLocal<Loan> threadLocal = new ThreadLocal<ThreadLocalGetMethod.Loan>();
public static void main(String[] args) {
//執行緒1
new Thread(() -> {
System.out.println("執行緒-1loan:"+threadLocal.get()); //輸出null
}).start();
}
@Data
@AllArgsConstructor
public static class Loan {
private String name;
private String amount;
}
}
按照上面的例子,你先new了一個ThreadLocal物件,所以需要看下ThreadLocal建構式,做了什么事情沒有,很明顯什么都沒做,
public ThreadLocal() {
}
之后執行緒1會直接執行了threadLocal.get操作,讓我們看下他的原始碼:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
你可以看到,上面的get方法的脈絡主要如下:
1. 獲取當前執行緒的一個變數ThreadLocalMap
2. 如果map為空呼叫setInitialValue回傳默認值,并創建map
3. 如果map非慷訓取entry中key的對應的value值
你可以先畫一個圖,之后再來分別看下每一步,threadLocal的get方法核心脈絡如圖所示:

ThreadLocal原始碼剖析get方法細節
## ThreadLocal原始碼剖析get方法細節
這個是總的脈絡圖,接下來看一下每一步的細節,
1、獲取當前執行緒的一個變數ThreadLocalMap,
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//暫時省略
}
這兩句代碼第一句是獲取到當前運行的執行緒物件,第二句獲取了如下map,可以從注釋看出來,實際就是獲取了thread物件t的一個屬性,這屬性是一個ThreadLocalMap,代碼如下:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
不知道各位還記得上一節的thread創建的場景么?當中有一些細節并沒有講,thread除了狀態、名字、執行緒id以外,還有兩個比較關鍵的屬性,threadLocals和inheritableThreadLocals,代碼如下:
public class Thread implements Runnable {
//......(其他原始碼)
/*
* ThreadLocal使用,當前執行緒的ThreadLocalMap,主要存盤該執行緒自身的ThreadLocal
*/
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal使用,自父執行緒繼承而來的ThreadLocalMap,
* 主要用于父子執行緒間ThreadLocal變數的傳遞
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
//......(其他原始碼)
}
注釋寫的清楚,一個用于是父子執行緒傳遞的變數副本Map,一般是InheritableThreadLocal才會使用,一個是自己執行緒變數副本Map一般ThreadLocal使用,
上一節還有一個細節我沒有講,inheritableThreadLocals這個變數在創建執行緒的時候呼叫init方法的時候會判斷,如果父執行緒有值復制到子執行緒一份,代碼如下:
/**
* 初始化一個執行緒.此函式有兩處呼叫,
* 1、上面的 init(),不傳AccessControlContext,inheritThreadLocals=true
* 2、傳遞AccessControlContext,inheritThreadLocals=false
*/
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
//......(其他代碼)
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
//......(其他代碼)
}
inheritableThreadLocals這個變數的應用在請求跟蹤,傳遞traceId的時候可以被用到,這里不做過多關注,核心還是關注threadLocals這個基本的執行緒變數副本,
你可以看下整個Map是什么?是一個ThreadLocalMap,它是ThreadLocal的內部類,所以你可以得到如下圖所示結論:

回過頭來再看下,這兩行代碼,非常關鍵的一點就是,雖然使用了ThreadLocal的get,但是操作的實際是當前執行緒的threadLocals本地變數副本的Map,這一點是很重要的,
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//暫時省略
}
getMap方法執行完成后,流程如下圖所示:

2、如果map為空,呼叫setInitialValue回傳默認值,并創建map
由于當前執行緒的獲取到副本變數map為null,所以會執行到setInitialValue這個分支,如下所示:

所以你要來看下setInitialValue的代碼:
private T setInitialValue() {
T value = https://www.cnblogs.com/fanmao/p/initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
你看完這個代碼后,可以發現這個方法核心脈絡做了兩件事情,一個是初始化值,一個是創建map,
如下圖所示:

首先上面第一句代碼是呼叫了initialValue方法,從名字上看就是一個初始化的動作,可以看下它的原始碼,非常簡單:
protected T initialValue() {
return null;
}
默認回傳null,可以通過重寫這個方法或者一個匿名內部類(jdk1.8)來設定一個初始值,
這里我給出大家設定初始值的方式,
使用重寫initialValue方式
private static ThreadLocal<Loan> threadLocal = new ThreadLocal<Loan>() {
@Override public Loan initialValue() {
return new Loan("默認值", "1000.00");
}
};
或者使用withInitial,匿名內部類
private static ThreadLocal<Loan> threadLocal = ThreadLocal.withInitial(() -> new Loan("默認值", "1000.00"));
setInitialValue執行到這里,邏輯很簡單,如下:

接著進行了if判斷,當前執行緒的本地變數副本threadLocals通過getMap獲取到的肯定默認是null,所以會執行創建Map如下圖:

所以會創建map,執行如下代碼
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
上面這個創建map 的細節我們不過去深入了,ThreadLocalMap不是我們要講的重點,有興趣的同學可以看下他的原始碼,這里給出ThreadLocalMap核心點:
- 底層層是陣列,默認大小16,默認擴容閾值10,擴容閾值計算方式:threshold = len * 2 / 3= 10
- key是threadLocal,vlaue是ThreadLocal的泛型物件
- 尋址演算法firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
- hash值演算法:firstKey.threadLocalHashCode,底層是用了AtomicInteger和一個增量值HASH_INCREMENT,保證
- Hash沖突使用開放尋址法(HashMap是單鏈表法)
- 開放尋址的核心是位置有元素了就換位置
這里value應該是為null,因為initialValue方法沒有指定初始化值,

最后我們回到get原始碼的脈絡圖,經過getMap()、setInitialValue()方法呼叫后,最終執行緒threadLocal.get()會輸出null的值,如下所示:

好了,今天ThreadLocal就學習到這里,下一節我們來探索下:
- ThreadLocal的set原始碼原理
- JVM的中的強參考、弱參考、軟參考、虛參考
- 弱參考在ThreadLocal的應用
- ThreadLocal記憶體泄漏問題分析
- ThreadLocal應用場景舉例
本文由博客群發一文多發等運營工具平臺 OpenWrite 發布
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/327756.html
標籤:Java
上一篇:HotSpot物件揭秘
