Android多執行緒之Java 8中ThreadLocal內部實現機制詳解
前言:ThreadLocal
是執行緒內部的儲存類,通過它可以實現在每個執行緒中儲存自己的私有資料。即資料儲存以後,只能在指定的執行緒中獲取這個儲存的物件,而其它執行緒則不能獲取到當前執行緒儲存的這個物件。ThreadLocal
有一個典型的應用場景,即我們在前文中說到的Android執行緒間通訊中的Looper。每一個執行緒都有一個私有的Looper
物件去處理當前執行緒的訊息佇列,有不清楚的同學,可以去上篇文章檢視。話不多說,今天我們主要探討的是ThreadLocal
實現執行緒儲存私有資料的工作原理。
- 上面我們提到,通過
ThreadLocal
能實現線上程中儲存的私有資料,下面我們來看一個典型的應用案例,我們在UI執行緒中執行如下程式碼:
private void testThreadLocal() {
final ThreadLocal<String> nameLocal = new ThreadLocal<>();
nameLocal.set("我是UI主執行緒儲存的資料");
new Thread(new Runnable() {
@Override
public void run() {
nameLocal.set("我是子執行緒儲存的資料");
//打印出當前執行緒和其儲存的資料
System.out.println(Thread.currentThread() + ":" + nameLocal.get());
}
}).start();
//打印出當前執行緒和其儲存的資料
System.out.println(Thread.currentThread() + ":" + nameLocal.get());
}
上面程式碼中,我們在主執行緒中建立了一個nameLocal
物件,並且向裡面寫入了一個字串。接下來,我們又開啟了一個新的子執行緒,又向同一個nameLocal
nameLocal
都是一個ThreadLocal
物件,因此呼叫get
方法去獲取存入的字串時,應該是一個相同的字串。但實際結果是怎樣?實際輸出結果如下:
03-12 15:49:49.726 19346-19346/? I/System.out: Thread[main,5,main]:我是UI主執行緒儲存的資料
03-12 15:49:49.726 19346-19358/? I/System.out: Thread[Thread-209,5,main]:我是子執行緒儲存的資料
通過列印的結果可以看出:在不同執行緒中,即使操作的同一個ThreadLocal物件,也能夠實現資料的私密儲存。但是,我們呼叫ThreadLocal的set方法的時候,操作的是同一個ThreadLocal物件,而且也沒有不同的Key去區分不同的value值,為什麼不會覆蓋上一次儲存的value?呼叫get方法為什麼能獲取到當前執行緒儲存的資料?帶著上面的兩個問題,我們一起走入ThreadLocal的原始碼世界,一探究竟。
ThreadLocal原始碼分析:
- ThreadLocal儲存值set方法詳解
從上面的示例可以看出,我們利用ThreadLocal
儲存資料的時候,只需要簡單的在ThreadLocal
物件上呼叫set(value)
方法,即可以實現資料的儲存,而且實現執行緒的區分,怎麼實現的?接下來進入ThreadLocal
的set
方法一探究竟:
public void set(T var1) {
//獲取呼叫set方法的當前執行緒
Thread var2 = Thread.currentThread();
//從執行緒中獲取當前執行緒中儲存的ThreadLocal的儲存物件ThreadLocalMap
ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
if(var3 != null) {
//如果以前對ThreadLocalMap進行過初始化,直接儲存
var3.set(this, var1);
} else {
//未進行過初始化,呼叫createMap方法先建立,再儲存資料
this.createMap(var2, var1);
}
}
set方法內部實現邏輯非常簡便清晰,先獲取到當前執行緒的ThreadLocalMap
物件,再通過這個物件去實現資訊的儲存。說起來簡單,但我們仍然對getMap
幹了什麼事情?set
方法和createMap
方法怎麼進行資訊儲存?有很大的疑惑,接下來我們一一解析。先看getMap
內部實現邏輯:
//ThreadLocal類中的getMap方法
ThreadLocal.ThreadLocalMap getMap(Thread var1) {
return var1.threadLocals;
}
//Thread類中的threadLocals變數定義,每個執行緒都有這個變數
ThreadLocalMap threadLocals = null;
//ThreadLocalMap類的定義
static class ThreadLocalMap {
//陣列的初始容量
private static final int INITIAL_CAPACITY = 16;
//儲存的ThreadLocal陣列,我們實現執行緒間私有化資料,就存放在這個陣列中
private ThreadLocal.ThreadLocalMap.Entry[] table;
//陣列的大小,不包括空資料
private int size;
//用於儲存陣列的總容量,包括為空的資料
private int threshold;
}
上邊羅列了getMap
方法的實現和它用到的ThreadLocalMap
類的定義。getMap
方法就是從當前執行緒中獲取它的ThreadLocalMap
成員變數而已。關於ThreadLocalMap
,我們現在只要知道它是一個儲存資料的物件即可,至於它內部實現機制,我們接下來會詳細講解。
接下來看一下ThreadLocalMap
中關鍵的set
方法是如何實現資料儲存的,原始碼如下:
private void set(ThreadLocal<?> var1, Object var2) {
//ThreadLocal.ThreadLocalMap.Entry的定義見該方法下,其實它也是ThreadLocal物件,只是是虛引用物件
ThreadLocal.ThreadLocalMap.Entry[] var3 = this.table;
//獲取陣列的大小
int var4 = var3.length;
//通過按位與運算,獲取var1在陣列中的儲存位置。
//補充(&類似於取模(%)運算,但是效率比%高很多,a%b可以用位運算計算:a&b-1)
int var5 = var1.threadLocalHashCode & var4 - 1;
//從陣列中取出ThreadLocal物件,相當於遍歷陣列中儲存的資料,只要取出來的資料不為空,就一直通過nextIndex方法獲取下一個位置的物件。一開始的時候,陣列為空,不會執行for迴圈中的程式碼。
for(ThreadLocal.ThreadLocalMap.Entry var6 = var3[var5]; var6 != null; var6 = var3[var5 = nextIndex(var5, var4)]) {
//當陣列不為空的時候,進入迴圈遍歷陣列操作,nextIndex方法就是取下一個位置
//獲取到從陣列中取出來的那個ThreadLocal物件
ThreadLocal var7 = (ThreadLocal)var6.get();
//如果以前通過var1物件儲存過資料,只更新其值,結束方法(var即是呼叫set方法的那個ThreadLocal物件)
if(var7 == var1) {
var6.value = var2;
return;
}
//遍歷完陣列,沒有找到以前通過var1儲存過資料的痕跡,就把資料儲存到陣列第一個為null的位置.結束方法
if(var7 == null) {
this.replaceStaleEntry(var1, var2, var5);
return;
}
}
//當陣列為空的時候,直接建立一個節點,並且新增到陣列中
var3[var5] = new ThreadLocal.ThreadLocalMap.Entry(var1, var2);
//下面的操作是對陣列進行重新計算操作
int var8 = ++this.size;
if(!this.cleanSomeSlots(var5, var8) && var8 >= this.threshold) {
this.rehash();
}
}
//Entry 物件的定義,比ThreadLocal多了一個value物件
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> var1, Object var2) {
super(var1);
this.value = var2;
}
}
最後,我們看一下通過ThreadLocal
儲存資料時,整個呼叫鏈:
//建立ThreadLocal物件
ThreadLocal<String> nameLocal = new ThreadLocal<>();
//呼叫set方法儲存資料
nameLocal.set("value");
//獲取當前執行緒儲存的ThreadLocalMap 物件
Thread var2 = Thread.currentThread();
ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
//呼叫set方法或createMap儲存資料
var3.set(this, var1);//存在同一個ThreadLocal物件
或
this.createMap(var2, var1);//不存在同一個ThreadLocal物件
//最後就是向table陣列中新增新的節點或更新舊結點
呼叫我們上面分析的set方法:
//當陣列為空時,直接呼叫如下程式碼新增節點
var3[var5] = new ThreadLocal.ThreadLocalMap.Entry(var1, var2);
//當陣列不為空時,迴圈更新或新增。更新為var6.value = var2覆蓋其值,新增是呼叫如下方法
this.replaceStaleEntry(var1, var2, var5);
ThreadLocal儲存值get方法詳解
相比通過
set
方法儲存資料來說,獲取資料的get
方法就要簡便得多。我們一般是通過呼叫nameLocal.get()
來獲取資料,我們就先看一下ThreadLocal
類的get
方法是怎麼獲取資料的。方法實現如下:
public T get() {
//首先還是拿到當前執行緒關聯的ThreadLocalMap物件
Thread var1 = Thread.currentThread();
ThreadLocal.ThreadLocalMap var2 = this.getMap(var1);
if(var2 != null) {
//從陣列中取到指定位置(即通過hashcode和陣列大小計算出的,和上面儲存時獲取var5類似)的那個Entry物件,方法詳情見下
ThreadLocal.ThreadLocalMap.Entry var3 = var2.getEntry(this);
if(var3 != null) {
//有資料就直接返回儲存的資料
Object var4 = var3.value;
return var4;
}
}
//當沒有儲存的有資料,就是設定資料為null並且返回。方法實現邏輯會在後面說到。
return this.setInitialValue();
}
ThreadLocalMap
的getEntry
方法是怎麼獲取到指定ThreadLocal.ThreadLocalMap.Entry
物件的?看一下內部實現:
private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> var1) {
//獲取在陣列中的位置
int var2 = var1.threadLocalHashCode & this.table.length - 1;
//取當前位置的資料
ThreadLocal.ThreadLocalMap.Entry var3 = this.table[var2];
return var3 != null && var3.get() == var1?var3:this.getEntryAfterMiss(var1, var2, var3);
}
ThreadLocalMap
的setInitialValue
方法又是怎麼去設定預設值並返回資料的?看一下它的原始碼:
private T setInitialValue() {
//this.initialValue()方法就是返回了一個null,因此var賦值為null
Object var1 = this.initialValue();
//獲取與當前執行緒相關聯的ThreadLocalMap物件
Thread var2 = Thread.currentThread();
ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
if(var3 != null) {
//上面講過方法的含義,這裡是重寫為null值
var3.set(this, var1);
} else {
//上面講過方法的含義,這裡是新增一個為null的新節點
this.createMap(var2, var1);
}
//返回null值
return var1;
}
//上面提到的initialValue()方法原始碼
protected T initialValue() {
return null;
}
最後,還是看一下完整的get
方法呼叫鏈:
//呼叫set方法從nameLocal中獲取資料
nameLocal.get();
//獲取當前執行緒儲存的ThreadLocalMap 物件
Thread var2 = Thread.currentThread();
ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
//如果var3為空,就建立一個ThreadLocalMap,並且給資料賦值為null,並返回。呼叫如下程式碼
return this.setInitialValue();、
//如果var3不為空,就獲取指定位置(通過hashcode計算而來,原理上面講過)的Entry物件
ThreadLocal.ThreadLocalMap.Entry var3 = var2.getEntry(this);
//返回entry中的資料
Object var4 = var3.value;
return var4;
總結:通過ThreadLocal
實現資料的儲存和獲取原理到現在已經告一段落了。它是怎麼實現資料的執行緒私有化?其實很簡單,主要是通過執行緒的私有成員變數ThreadLocalMap
實現的,而ThreadLocalMap
中又有一個ThreadLocal.ThreadLocalMap.Entry
[]實現資料的儲存。每次我們儲存或獲取資料都是對這個陣列進行操作而已。關於Android多執行緒的詳細講解,大家可以去 Android多執行緒相關知識總結 檢視。希望能幫助到你額。。。