ThreadLocal原理分析
概述
ThreadLocal是面試非常高頻的問題,在很多框架原始碼中都可以看到他的身影,比如Spring,ReentrantReadWriteLock,然後在平時的工作使用的卻並不多,ThreadLocal要解決並不是多執行緒修改共享變數保證執行緒安全的問題,這個是通過悲觀鎖(比如synchronized)或者樂觀鎖(比如CAS)實現的,它要解決的問題是多執行緒環境下修改變數,每個執行緒修改自己的變數副本,執行緒之間互相不影響的問題。本文就介紹一下ThreadLocal是如何實現執行緒之間隔離的。
舉例
為了方便ThreadLocal的理解,這裡先舉一個ThreadLocal的使用小例子,通過例子來分析它的原理。
public class SeqCount { // 一般使用private static修飾 private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>(){ // 實現initialValue() public Integer initialValue() { return 0; } }; public int nextSeq(){ seqCount.set(seqCount.get()+ 1); return seqCount.get(); } public static void main(String[] args){ SeqCount seqCount = new SeqCount(); SeqThread thread1 = new SeqThread(seqCount); SeqThread thread2 = new SeqThread(seqCount); SeqThread thread3 = new SeqThread(seqCount); SeqThread thread4= new SeqThread(seqCount); thread1.start(); thread2.start(); thread3.start(); thread4.start(); } private static class SeqThread extends Thread{ private SeqCount seqCount; SeqThread(SeqCount seqCount){ this.seqCount = seqCount; } public void run() { for(int i = 0 ; i < 3 ; i++){ System.out.println(Thread.currentThread().getName() + " seqCount值為 :" + seqCount.nextSeq()); } } } }
執行結果
Thread-0 seqCount值為 :1 Thread-0 seqCount值為 :2 Thread-0 seqCount值為 :3 Thread-1 seqCount值為 :1 Thread-1 seqCount值為 :2 Thread-1 seqCount值為 :3 Thread-2 seqCount值為 :1 Thread-2 seqCount值為 :2 Thread-2 seqCount值為 :3 Thread-3 seqCount值為 :1 Thread-3 seqCount值為 :2 Thread-3 seqCount值為 :3
為了對比,把上面的例子修改一下,不使用ThreadLocal看一下執行結果是怎麼樣的。
public class SeqCount1 { private static AtomicInteger seqCount1 = new AtomicInteger(0); public int nextSeq(){ return seqCount1.incrementAndGet(); } public static void main(String[] args){ SeqCount1 seqCount = new SeqCount1(); SeqThread thread1 = new SeqThread(seqCount); SeqThread thread2 = new SeqThread(seqCount); SeqThread thread3 = new SeqThread(seqCount); SeqThread thread4 = new SeqThread(seqCount); thread1.start(); thread2.start(); thread3.start(); thread4.start(); } private static class SeqThread extends Thread{ private SeqCount1 seqCount; SeqThread(SeqCount1 seqCount){ this.seqCount = seqCount; } public void run() { for(int i = 0 ; i < 3 ; i++){ System.out.println(Thread.currentThread().getName() + " seqCount值為 :" + seqCount.nextSeq()); } } } }
執行結果為:
Thread-0 seqCount值為 :1 Thread-0 seqCount值為 :2 Thread-0 seqCount值為 :3 Thread-1 seqCount值為 :4 Thread-1 seqCount值為 :5 Thread-1 seqCount值為 :6 Thread-2 seqCount值為 :7 Thread-2 seqCount值為 :9 Thread-2 seqCount值為 :10 Thread-3 seqCount值為 :8 Thread-3 seqCount值為 :11 Thread-3 seqCount值為 :12
通過上面兩個例子大家可以清楚的看到,不使用ThreadLocal就變成了一個執行緒同步的問題,而使用了ThreadLocal之後執行緒之間就沒有協作的問題,而是每個執行緒修改自己的變數副本,變數變成了執行緒內部私有的變數。
ThreadLocalMap
在上面的例子中,大家會發現使用ThreadLocal的get()、set()方法,而這些方法最後要操作就是ThreadLocalMap,所以這裡先介紹一下這個東東,這個map是聯絡ThreadLocal和Thread的橋樑,當分析完這個map,大家對Thread,ThreadLocal,ThreadLocalMap之間的關係就會變得非常清晰。
ThreadLocalMap屬性分析
//ThreadLocalMap是通過Entry實現的key-value儲存 static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } //ThreadLocalMap初始容量 private static final int INITIAL_CAPACITY = 16; //儲存Entry的陣列 private Entry[] table; //ThreadLocalMap中元素個數 private int size = 0; //ThreadLocalMap的負載因子 private int threshold;
針對上面的屬性,做下面幾點解讀:
- 看過HashMap原始碼的應該有印象,HashMap實現了Map介面,而且在Map介面中也有一個Entry介面,HashMap是通過Node來儲存key-value的,Node實現了Entry介面。ThreadLocalMap卻完全不同,它既沒有實現Map介面,在Entry中也沒有類似next的指標指向下一個節點,說明ThreadLocalMap中沒有使用連結串列,就直接儲存在陣列上,除此之外,ThreadLocalMap是ThreadLocal的內部類,沒有使用public修飾,預設是隻有當前包下面的類才可以使用,也就是說這個Map我們自己寫的程式碼中是不能直接建立的。
- Entry中的key就是ThreadLocal,而且這個ThreadLocal還被WeakReference包裝了一下,也就是說ThreadLocal在這裡是弱引用,如果ThreadLocal為null,可以直接被gc垃圾回收,關於弱引用,後面會舉一個簡單的例子,大家看一下即可。具體可以參考:用弱引用堵住記憶體洩漏
- 下面幾個屬性和HashMap中類似,這裡有意思的一點是HashMap的負載因子是0.75,而ThreadLocalMap的負載因子是2/3。
弱引用使用舉例
public class FinalizeTest { @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize methode executed"); } public static void main(String[] args) { FinalizeTest finalizeTest = new FinalizeTest(); WeakReference<FinalizeTest> weak = new WeakReference(finalizeTest); Map<WeakReference<FinalizeTest>,Integer> map = new HashMap<>(); map.put(weak,1); System.out.println("====第一次gc"); System.gc(); finalizeTest = null; System.out.println("====第二次gc"); System.gc(); } }
執行結果
====第一次gc ====第二次gc finalize methode executed
這裡為了模擬ThreadLocalMap,也搞了一個Map,這個map的key也是一個使用WeakReference包裝的類,事實上這個map中key的引用並沒有影響gc垃圾回收,只要將物件finalizeTest設定為null,就可以正常垃圾回收,所以ThreadLocalMap中Entry節點的key的垃圾回收也是如此。ThreadLocalMap使用弱引用是為了解決記憶體洩漏的問題,至於什麼是記憶體洩漏,參考:對ThreadLocal實現原理的一點思考。
ThreadLocalMap構造方法分析
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { //初始化陣列,容量大小為16 table = new Entry[INITIAL_CAPACITY]; //通過key的hash值和15做與運算得到桶的位置 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //將key-value封裝到Entry中插入陣列 table[i] = new Entry(firstKey, firstValue); size = 1; //設定閾值,達到這個閾值就擴容,閾值為16 * (2/3),當然這裡要取整 setThreshold(INITIAL_CAPACITY); }
構造方法很簡單,就不過多介紹了。
ThreadLocalMap常用方法分析
//由於ThreadLocalMap不像HashMap,發生Hash衝突時使用連結串列解決,ThreadLocalMap的做法就是發生hash衝突 //會找當前位置的下一個桶 private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } //當前位置的上一個位置 private static int prevIndex(int i, int len) { return ((i - 1 >= 0) ? i - 1 : len - 1); } private Entry getEntry(ThreadLocal<?> key) { //確定桶的位置 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; //如果找的位置entry不為null,並且entry正好是要找的key,就返回 if (e != null && e.get() == key) return e; else //這一步其實就是發生了hash衝突,本來應該是這個key佔用的位置,卻被別的key給佔用了 //所以這裡就要去陣列挨個找了 return getEntryAfterMiss(key, i, e); } //通過key的hash定位到桶中entry,entry中的key和自己的key不相同,就會呼叫這個方法 //引數中的i就是key通過hash定位到在桶中的位置 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null) //在這個方法中會把key對應的value給置為null,同時將entry移除 expungeStaleEntry(i); else //如果當前桶中的entry不符合,就找後一個節點 i = nextIndex(i, len); e = tab[i]; } return null; } //向map中插入元素 private void set(ThreadLocal<?> key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); //這裡使用了一個for迴圈,尋找定位到的桶,如果定位到的桶中有元素 //就尋找該桶之後沒有存放元素的桶用來存放當前的key for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); //如果key重複,用新的value覆蓋舊的value if (k == key) { e.value = value; return; } if (k == null) { //在這個方法中會檢測key是否為null,如果為null就把value也置為null //同時移除Entry節點 replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; //擴容成原來的2倍 int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen]; int count = 0; //將舊陣列中的元素賦值到新陣列中 for (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null) { ThreadLocal<?> k = e.get(); if (k == null) { //上面介紹key通過弱引用包裝,可以正常GC,但是value沒有使用弱引用 //所以在key被垃圾回收之後,value並不會被回收,所以這裡手動設定為null //為了幫助垃圾回收 e.value = null; // Help the GC } else { //重新定位元素在新陣列中的位置 int h = k.threadLocalHashCode & (newLen - 1); while (newTab[h] != null) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } setThreshold(newLen); size = count; table = newTab; }
上面都有註釋,這裡提幾點需要注意的地方
- 由於ThreadLocalMap沒有使用散列表的結構,所以發生hash衝突的時候是尋找下一個桶
- 把key使用弱引用,可以使得gc正常回收,但是value並不是弱引用,所以在擴容的時候,把value置為null,方便value垃圾回收,在平時寫程式碼的時候,如果某個ThreadLocal不在使用了,最好直接呼叫ThreadLoalMap的remove方法把當前的key,value都移除,防止記憶體洩漏
- 在getEntry方法和set方法中當key為null,就把value也置為null,同時把Entry也移除了。
- 裡面有些方法沒有詳細註釋,因為並不是重要方法,所以就沒有仔細看
Thread、ThreadLocal、ThreadLocalMap三者之間的的關係
在我的另一篇分析Thread的文章有提到在Thread原始碼中有這麼一個欄位,如下:
ThreadLocal.ThreadLocalMap threadLocals = null;
這個欄位就是儲存TreadLocalMap的,也就是說每個執行緒都有一個ThreadLocalMap,ThreadLocalMap中儲存這個ThreadLocal和ThreadLocal封裝的成員變數的值,同一個父執行緒的子執行緒的ThreadLocalMap中儲存的ThreadLocal都是一樣的,只是value不同,具體三者之間的關係可以用下圖表示。
ThreadLocal常用方法分析
get方法
public T get() {
//獲取當前執行緒引用 Thread t = Thread.currentThread();
//獲取當前執行緒的ThreadLocalMap,就是上面介紹的Thread類中的threadLocals欄位 ThreadLocalMap map = getMap(t); if (map != null) {
//拿到ThreadLocalMap之後,根據key獲取Entry ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } }
//如果是首次插入,map沒有建立,建立ThreadLocalMap return setInitialValue(); }
進入#getMap()方法
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
進入#setInitialValue()方法
private T setInitialValue() { //這個方法在最開始舉例的時候重寫了 T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else //建立map createMap(t, value); return value; }
進入#initialValue()方法
protected T initialValue() { return null; }
這個返回的泛型T就是ThreadLocal要包裝的成員變數
進入#createMap()方法
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
直接呼叫上面分析的ThreadLocalMap的構造方法建立,並且給Thread中threadLocals賦值,從這裡開始Thread就和ThreadLocal還有ThreadLocalMap聯絡起來了。
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); }
這個方法很簡單,就不分析了。
remove方法
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
這個也很簡單。
常見應用場景
由於工作中基本沒有使用過,所以在網上看到幾個常見的使用場景,如下:
- 把session儲存到ThreadLocal中,但是現在session一般儲存在redis中,用於分散式共享,使用ThreadLocal只能在一個節點的執行緒中共享,無法做到分散式共享,所以這個場景目前來看並不合適。
- 由於SimpleDateFormat在格式化時間的時候,執行緒不安全,所以在高併發的時候格式化出來的日期可能是錯誤的,可以使用ThreadLocal封裝SimpleDateFormat,避免每次重新建立這個物件,這個確實是一個使用場景,但是現在是java8的天下,完全可以不用這個格式化類,java8可以通過LocalDateTime獲取日期時間,通過DateTimeFormatter進行格式化,這個是一個執行緒安全的類
沒有找到具體日常開發中使用ThreadLocal的場景,所以找到了原始碼中使用ThreadLocal的例子,就是ReentrantReadWriteLock,是一個讀寫鎖,大家有興趣可以看一下我的另一篇文章:ReentrantReadWriteLock原理分析
參考: