本篇文章主要講解 ThreadLocal 的用法和內部的數據結構及實現。有時候我們寫代碼的時候,不太注重類之間的職責划分,經常造出一些上帝類,也就是什么功能都往這個類里放。雖然能實現功能但是並不優雅且不好維護。這篇文章就介紹 ThreadLocal 中如何設計優雅的數據結構以及類之間的職責划分,至於怎么跟農夫山泉廣告語扯上關系,相信你讀完便有了答案,文末也有解釋。
用法
ThreadLocal 對象可以當做每個線程局部變量。也就是不同線程同時讀寫同一個 ThreadLocal 對象,其實操作的是線程本地的數據, 所以不存在線程安全問題。聽起來跟我們常規的理解有點矛盾,下面就舉個例子看看
package com.cnblogs.duma.thread; public class ThreadLocalTest { // 定義 ThreadLocal 對象 static ThreadLocal<String> var1 = new ThreadLocal<String>(); public static void main(String args[]) { new Thread1().start(); new Thread1().start(); } public static class Thread1 extends Thread { @Override public void run() { // 寫同一個 ThreadLocal 對象 —— var1 var1.set(getName()); while (true) { System.out.println(getName() + " get var1: " + var1.get()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
控制台輸出
Thread-0 get var1: Thread-0 Thread-1 get var1: Thread-1 Thread-1 get var1: Thread-1 Thread-0 get var1: Thread-0 Thread-1 get var1: Thread-1 Thread-0 get var1: Thread-0
本例中,我們定義了 ThreadLocal 對象 static ThreadLocal<String> var1 = new ThreadLocal<String>(); ,一般定義 ThreadLocal 對象都用 static 修飾,因為它的作用是提供讀寫線程本地屬性的能力,與對象沒關系,所以定義成 static 即可。從控制台輸出可以看出,所有線程都讀寫 var1 變量, 但互不干擾。看到這里,你可能會猜想,是不是 ThreadLocal 的 set 方法將線程做 key(每個線程都是獨一無二的),將我們要設置的值作為 value,存到一個 公共的 map 結構中,如果這樣想只對了一半, 下面我會對照源碼詳細介紹。
看到這里你可以又有疑問,為什么不在線程內部定義個局部變量,不是也能實現 ThreadLocal 的功能。為了解答這個問題,我們來看看下面的例子
package com.cnblogs.duma.thread; public class ThreadLocalTest { // 定義 ThreadLocal 對象 static ThreadLocal<String> var1 = new ThreadLocal<String>(); public static void main(String args[]) { new Thread1().start(); new Thread1().start(); new Thread2().start(); } public static class Thread1 extends Thread { @Override public void run() { // 寫同一個 ThreadLocal 對象 —— var1 var1.set(getName()); while (true) { System.out.println(getName() + " get var1: " + var1.get()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static class Thread2 extends Thread { @Override public void run() { // 寫同一個 ThreadLocal 對象 —— var1 var1.set(getName()); while (true) { System.out.println(getName() + " get var1: " + var1.get()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
控制台輸出
Thread-0 get var1: Thread-0 Thread-2 get var1: Thread-2 Thread-1 get var1: Thread-1 Thread-0 get var1: Thread-0 Thread-2 get var1: Thread-2 Thread-1 get var1: Thread-1
這個例子是在第一個基礎上又定義了一個線程類 —— Thread2,它同樣可以操作 var1 並且也是操作本地的變量,與 Thread1 線程互不干擾。所以,我們就知道 ThreadLocal 與局部變量的區別了, 它可以為所有線程提供本地數據的讀寫能力。
原理
既然用法了解了,那我們就深入到 ThreadLocal 類的內部一探究竟,看看它的實現跟我們剛才的猜測有什么區別。在 ThreadLocal 中最重要的兩個方法是 set 和 get,下面我們分別看下這兩個方法的源碼
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); } // 獲取當前線程的 threadLocals 屬性 ThreadLocalMap getMap(Thread t) { return t.threadLocals; } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
在 set 方法中, 首先調用了 getMap 方法,getMap 方法返回的是當前線程的 threadLocals 屬性,如果當前線程的 threadLocals 屬性為空,需要初始化,即調用 createMap 方法,在該方法中,new 一個 ThreadLocalMap 對象,該類是 ThreadLocal 的內部類。因此,我們可以看到並不是有一個全局的 map 保存數據,而確實是每個線程本地的局部變量(threadLocals)。既然每個線程操作是屬於自己的變量,那么把 ThreadLocalMap 對象放在線程本地自然就更合理。接下來看看 ThreadLocalMap 的構造方法
public class ThreadLocal<T> { private final int threadLocalHashCode = nextHashCode(); private static AtomicInteger nextHashCode = new AtomicInteger(); private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } static class ThreadLocalMap { private static final int INITIAL_CAPACITY = 16; private java.lang.ThreadLocal.ThreadLocalMap.Entry[] table; private int size = 0; static class Entry extends WeakReference<java.lang.ThreadLocal<?>> { Object value; Entry(java.lang.ThreadLocal<?> k, Object v) { super(k); value = v; } } ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { // table 用來存儲不同 ThreadLocal 對象對應的值 table = new Entry[INITIAL_CAPACITY]; // 每一個 ThreadLocal 對象都有一個 threadLocalHashCode 屬性,即 hash 編碼,與之對應 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // Entry 對象存儲 ThreadLocal 對象以及與該 ThreadLocal 對象對應的值 table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); } } }
該構造方法中會用 INITIAL_CAPACITY 屬性初始化 table,該屬性是個數組,且元素是 Entry 類型(ThreadLocalMap 的內部靜態類)。這里不知道你是不是有疑問,我們調用 set 是只傳一個值,為什么這里初始化一個數組存放數據。是因為當程序中不止一個 var1,還有 var2、var3 等多個 ThreadLocal 變量時,就需要一個數組(table)來存儲不同 ThreadLocal 對象及其對應的值。
接下來分別說說 Entry 類和 ThreadLocal 類中的屬性。Entry類包含兩個屬性,一個是 ThreadLocal 對象,另一個是需要 set 的值。ThreadLocal 中有一個屬性——threadLocalHashCode,代表 ThreadLocal 對象的 hash 編碼。由源碼可知,threadLocalHashCode 屬性是由 nextHashCode 生成,nextHashCode 是 AutomicInteger 類型,是線程安全的。
在 firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 這段代碼中,用 firstKey(即 ThreadLocal 對象)的 hash 編碼與 (INITIAL_CAPACITY - 1) 做按位與操作,就能生產 [0,INITIAL_CAPACITY - 1] 區間的一個數字 i, 將生成的 Entry 對象存入 table[i] 中。小結一下這里的邏輯:每個 ThreadLocal 對應一個 hash 編碼,每個 hash 編碼可以生成一個下標 i,將 ThreadLocal 對象與待 set 的值生成 Entry 對象,存儲到 table[i] 中。這一切都是在操作當前線程的局部屬性 —— threadLocals,因此跟其他線程沒有關系。
當 getMap(t) 方法返回的 map 不為空,則執行 map.set(this, value); 方法,ThreadLocalMap 的 set 方法如下:
private void set(ThreadLocal<?> key, Object value) { 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)]) { ThreadLocal<?> k = e.get(); // 當前位置是之前設置的,value 直接覆蓋 if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } // 此時 tab[i] 為空,存儲 Entry 對象 tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
該方法大致處理邏輯為:首先,生成 key 對象的下標 i,生成方法與之前介紹的一樣。因為不同的 ThreadLocal 都生成 [0, len-1] 區間的下標,可能會導致多個不同的 ThreadLocal 對象生成的下標 i 是一樣的,產生 Hash 碰撞,也就是說兩個 hash 編碼搶占同一個位置,對於該例子處理 hash 碰撞的方法為開放地址法,即:從當前的位置繼續向下尋找下一個位置(例子中 for 循環的 nextIndex(i, len) 語句),直到找到的位置為空,則存下當前的值。解決 Hash 碰撞的方法除了開發地址法,還有再 Hash 法和拉鏈法有興趣的朋友可以自行查閱。
get 方法
理解 set 方法后,在看 get 方法就容易了,思路都是一致的。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(); } private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; }
如果 getMap(t) 返回值為空,這說明沒有初始化過,則調用 setInitialValue 方法進行初始化。否則,執行 map.getEntry(this); 語句,代碼如下:
private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); ThreadLocal.ThreadLocalMap.Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); }
與 set 代碼類似,首先生成下標 i,因為存在 Hash 碰撞,也就是說下標 i 存儲的 ThreadLocal 對象不一定是方法形參 key,因此需要在 if 語句中增加 e.get() == key 判斷。
小結
本篇文章介紹了 ThreadLocal 的使用及原理。每個線程設置 ThreadLocal 時,其實都是設置每個線程本地的屬性,而 ThreadLocal 不存儲與線程相關的變量。ThreadLocal 只提供了讀寫方法,這樣做的好處是職責清晰,降低耦合度。有點像農夫山泉的廣告語,ThreadLocal 不生產數據,只是線程的搬運工。寫這篇文章的時候還感着冒,如表述不當,煩請指正。
歡迎關注公眾號「渡碼」,分享更多優秀書籍的筆記