這是ThreadLocal系列的最後一篇文章。
前幾篇文章更多的是在使用層面去介紹ThreadLocal,並沒有深入去理解原理。
其實學任何技術都是這樣一個過程,我們最先接觸到的可能是一個框架的API,然後你可能就會開始使用它;再然後會看看別人是怎麼使用它的,有沒有值得借鑑之處,再然後就是深入原理,看看它的底層是如何實現的,對它做一個深入的瞭解。
下面我們進入正題,先分析一下ThreadLocal幾個重要的方法。
set
set方法其實很短,我們先看一下程式碼:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
複製程式碼
先拿到當前的執行緒,然後通過它去拿到一個Map,如果這個Map存在,就把value塞進去,否則就建立一個新的。
ThreadLocalMap
是在ThreadLocal類裡面實現的一個Map,它的Entry是一個弱引用的實現。
static class Entry extends WeakReference<ThreadLocal<?>>
複製程式碼
每個執行緒對應一個自己執行緒私有的ThreadLocalMap,它被Thread物件持有:
// 類Thread裡面定義了ThreadLocalMap的引用
ThreadLocal.ThreadLocalMap threadLocals = null;
複製程式碼
從set方法的程式碼可以看到,最開始執行緒的threadLocals
可能是空,這個時候就建立一個新的,賦值給當前執行緒物件:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
複製程式碼
get
看完上面的分析後,get方法就很好理解了。仍然是先通過getMap
方法拿到當前執行緒對應的Map,然後從裡面取出value。如果沒有value,就呼叫ThreadLocal提供的初始化方法,初始化一個值。
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();
}
複製程式碼
初始化
先來看在ThreadLocal定義的的初始化方法,看起來就是一個很簡單的protected
方法:
protected T initialValue() {
return null;
}
複製程式碼
而為了更方便使用者使用,ThreadLocal自己內部有一個ThreadLocal的實現類,它提供了一個函數語言程式設計的方式來讓客戶端更方便地使用:
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
return supplier.get();
}
}
複製程式碼
我們甚至可以依樣畫葫蘆,自己新建一個FunctionedThreadLocal,實現更多的定製化。
remove
remove方法不得不提。首先我們思考一下,既然已經有了弱引用,按理說,如果執行緒沒有持有某個value的時候,會在GC的時候自動清理掉對應的Entry,為什麼會有remove方法存在?
因為我們在開發一個多執行緒的程式時,往往會使用執行緒池。而執行緒池的功能就是執行緒的複用。那如果執行緒池和ThreadLocal在一起就可能會造成一個問題:
job A和job B共用了同一個執行緒, job A使用完ThreadLocal,ThreadLocal裡面還有job A儲存的值,而這個時候可能還沒有清理掉, job B複用執行緒進來了,取出來是 job A的值,可能就會造成問題。
所以在有必要的時候,可以在使用完ThreadLocal的時候,顯式呼叫一下remove方法。remove方法的原始碼也比較簡單,就是呼叫對應的entry的clear
方法。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
複製程式碼
學習借鑑
這裡總結出我們從ThreadLocal的設計中可以學習借鑑的一些點。
避免併發
這個其實挺有意思的。如果讓我們自己來設計一個ThreadLocal,想要拿到當前執行緒對應的ThreadLocalMap,可能就會用一個Map來存這個關係:
Map<Thread, ThreadLocalMap> threadLocalMaps = new ConcurrentHashMap<>();
複製程式碼
因為可能會有多個執行緒同時呼叫get/set方法,所以還需要對這個Map來做一些措施來保證執行緒安全,比如使用ConcurrentHashMap,甚至複雜的原子操作可能還需要上鎖。這樣其實對效能是不利的。
而JDK巧妙地把這個引用直接放到了Thread
物件裡面,使得多個執行緒不需要同時操作同一個物件。所以我們在設計程式碼的時候,也要有這種思維的轉變。並不是說我想實現一個工具類,就一定要把所有的程式碼都寫在這個工具類裡面。要充分考慮怎樣設計更合理,效能更高。
弱引用
使用弱引用可以讓GC及時回收掉程式中不需要使用的物件。這個剛好適用於ThreadLocal的場景。因為很多時候執行緒執行完後,就銷燬了。如果讓我們顯示去呼叫一個方法,就會變得非常麻煩。而且一旦忘記回收,還有可能造撐滿記憶體。
所以這點ThreadLocal做得很好,利用了弱引用的特性,與Java的設計哲學一致:你只管用,回收的事情我幫你做了!
❝Tips: 這裡需要注意上面提到的與執行緒池一起使用可能存在的問題哦。
❞
簡單設計
ThreadLocal中自己定義了一個很簡單的可以自動擴容的Map。它處理衝突的方式與HashMap不一樣,HashMap是陣列 + 連結串列/紅黑樹的方式來處理雜湊衝突,而ThreadLocal實現得更簡單,使用的是「開放地址法」,如果發生了衝突,就尋找下一個有空的位置。
開放地址法雖然效率不一定高,但勝在實現起來很簡單,用在這裡綽綽有餘。我們在設計資料結構和演算法的時候,甚至是在設計程式的時候,也有遵循夠用、簡單就行的原則,不用太過度設計。也就是我們常說的KISS原則:Keep it stupid and simple。
函數語言程式設計
使用函數語言程式設計可以讓客戶端更簡單地實現定製化。比如ThreadLocal中的初始化方法,如果沒有函數語言程式設計,我們首先得新建一個ThreadLocal的繼承類,然後複寫它的initialValue
方法,用起來特別不方便。
我們在設計自己的工具類的時候,想要實現一定程度的靈活性和定製化,就可以考慮利用函數語言程式設計的便利。
巧用this
this
其實我們平時用的還算比較多,最多的地方應該是POJO類了。但ThreadLocal進行了一個騷操作。
我們看ThreadLocalMap的原始碼可以發現,它的key型別就是ThreadLocal。我們在呼叫get/set方法的時候,就會使用this。
為什麼要這麼設計?你會發現Thread和ThreadLocal其實是「多對多的關係」。一個Thread可能會用到多個ThreadLocal,而一個ThreadLocal又同時給多個Thread用。那麼問題來了,我們的入口是ThreadLocal物件,那如何能夠快速地拿到當前Thread,當前ThreadLocal的value?
這就是this
的關鍵之處了,我先拿到當前Thread,然後通過Thread裡面儲存的引用,拿到ThreadLocalMap,這個Map裡面儲存了此執行緒對應的所有ThreadLocal的物件,key就是這個物件本身,所以用this
作為key,可以快速找到當前ThreadLocal對應的value。
假如我們要實現一個多對多的場景,比如一個學生有多個老師,一個老師有多個學生。通過學生類作為入口進去,如何能夠快速獲取一個學生指定老師的分數?我們寫個程式來模擬一下:
// 教師類
public class Teacher {
// 每個教師儲存了自己每個學生的分數
Map<Student, Integer> scores = new HashMap<>();
public Map<Student, Integer> getScores() {
return scores;
}
}
// 學生類
public class Student {
public int get(Teacher teacher) {
Map<Student, Integer> scores = teacher.getScores();
return scores.get(this);
}
public void set(Teacher teacher, int score) {
teacher.getScores().put(this, score);
Map<Student, Integer> scores = teacher.getScores();
}
}
複製程式碼
當然了,這種場景其實並不多見。但ThreadLocal有它的特殊性,首先當前Thread物件是可以通過全域性直接獲取到的,然後我們的操作入口一般是ThreadLocal,使用而不是Thread。
試想一下,其實如果JDK開放許可權,通過Thread也能拿到最後的ThreadLocal,無非就是麻煩一些:大概長這樣:
Thread thread = Thread.currentThread();
// 如果jdk提供下面這個方法
ThreadLocalMap threadLocalMap = thread.getThreadLocals();
threadLocalMap.set(threadLocal, value); // set
Object value = threadLocalMap.get(threadLocal); // get
複製程式碼
但是這樣一看,顯然不如現在這樣設計得優雅:
threadLocal.set(value); //set
Object value = threadLocal.get(); // get
複製程式碼
所以這就是程式設計的哲學,大佬設計出來的東西,就是好用!JDK把ThreadLocal的引用放到了Thread裡面,讓它能夠避免多個執行緒爭用資源,再巧妙利用了this關鍵字,讓你可以很簡單地使用它。然後還考慮到了記憶體回收的問題,用弱引用幫你解決。
看完ThreadLocal原始碼不禁驚呼:只怪自己沒文化,一句臥槽走天下!
關於作者
我是Yasin,一個有顏有料又有趣的程式設計師。
微信公眾號:編了個程
個人網站:https://yasinshaw.com
關注我的公眾號,和我一起成長~