理解ThreadLocal之原理簡介
用途
我們一般用ThreadLocal來提供執行緒區域性變數。執行緒區域性變數會在每個Thread內擁有一個副本,Thread只能訪問自己的那個副本。文字解釋總是晦澀的,我們來看個例子。
public class Test { private static ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { Thread thread1 = new MyThread("lucy"); Thread thread2 = new MyThread("lily"); thread1.start(); thread2.start(); } private static class MyThread extends Thread { MyThread(String name) { super(name); } @Override public void run() { Thread thread = Thread.currentThread(); threadLocal.set("i am " + thread.getName()); try { //睡眠兩秒,確保執行緒lucy和執行緒lily都呼叫了threadLocal的set方法。 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(thread.getName() + " say: " + threadLocal.get()); } } }
這個例子非常簡單,就是建立了lucy和lily兩個執行緒。線上程內部,呼叫threadLocal的set方法存入一字串,睡眠2秒後輸出執行緒名稱和threadLocal中的字串。我們執行這單程式碼,看一下輸出內容。
lucy say: i am lucy lily say: i am lily
原理
上面例子很好的解釋了ThreadLocal的作用,接下來我們分析一下這是如何實現的。
我們定位到ThreadLocal的set方法。原始碼中set方法被拆分為幾個方法,為了表述方便筆者將這幾個方法進行了整合。
public void set(T value) { //獲取當前執行緒 Thread t = Thread.currentThread(); //獲取當前執行緒的ThreadLocalMap ThreadLocalMap map = t.threadLocals; if (map != null) //將資料放入ThreadLocalMap中,key是當前ThreadLocal物件,值是我們傳入的value。 map.set(this, value); else //初始化ThreadLocalMap,並以當前ThreadLocal物件為Key,value為值存入map中。 t.threadLocals = new ThreadLocalMap(this, value); }
通過上面這段程式碼可以看到,ThreadLocal的set方法主要是通過當前執行緒的ThreadLocalMap實現的。ThreadLocalMap是一個Map,它的key是ThreadLoacl,value是Object。
TreadLocal的get方法的原始碼我就不貼出來了,大體上與set方法類似,就是先獲取到當前執行緒的ThreadLocalMap,然後以this為key可以取得value。
到這裡我們基本上明白了ThreadLocal的工作原理,我們總結一下
- 每個Thread例項內部都有一個ThreadLocalMap,ThreadLocalMap是一種Map,它的key是ThreadLocal,value是Object。
- ThreadLocal的set方法其實是往當前執行緒的ThreadLocalMap中存入資料,其key是當前ThreadLocal物件,value是set方法中傳入的值。
- 使用資料時,以當前ThreadLocal為key,從當前執行緒的ThreadLocalMap中取出資料。
ThreadLocalMap
上面我們介紹了ThreadLocal主要是通過執行緒的ThreadLocalMap實現的。
static class ThreadLocalMap { private ThreadLocal.ThreadLocalMap.Entry[] table; static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> var1, Object var2) { super(var1); this.value = var2; } } }
ThreadLocalMap是一種Map,其內部維護著一個Entry[]。
ThreadLocalMap其實是就是將Key和Value包裝成Entry,然後放入Entry陣列中。我們看一下它的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(); if (k == key) { //如果已經存在,直接替換value e.value = value; return; } if (k == null) {//如果當前位置的key ThreadLocal為空,替換key和value。 replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value);//該位置沒有資料,直接存入。 int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) //檢查是否擴容 rehash(); } private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }
到這裡,如果你瞭解HashMap,應該可以看出ThreadLocalMap就是一種HashMap。不過它並沒有採用java.util.HashMap中陣列+連結串列的方式解決Hash衝突,而是採用index後移的方式。
我們簡單分析一下這段程式碼:
-
通過ThreadLocal的threadLocalHashCode與當前Map的長度計算出陣列下標 i。
-
從i開始遍歷Entry陣列,這會有三種情況:
-
Entry的key就是我們要set的ThreadLocal,直接替換Entry中的value。
-
Entry的key為空,直接替換key和value。
-
發生了Hash衝突,當前位置已經有了資料,查詢下一個可用空間。
-
-
找到沒有資料的位置,將key和value放入。
-
檢查是否擴容。
我們知道,HashMap是一種get、set都非常高效的集合,它的時間複雜度只有O(1)。但是如果存在嚴重的Hash衝突,那HashMap的效率就會降低很多。我們通過上段程式碼知道,ThreadLocalMap是通過 key.threadLocalHashCode & (len-1)計算Entry存放index的。len是當前Entry[]的長度,這沒什麼好說的。那看來祕密就在threadLocalHashCode中了。我們來看一下threadLocalHashCode是如何產生的。
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); } }
這段程式碼非常簡單。有個全域性的計數器nextHashCode,每有一個ThreadLocal產生這個計數器就會加0x61c88647,然後把當前值賦給threadLocalHashCode。關於0x61c88647這個神奇的常量,可以點這裡。