1. 程式人生 > 其它 >ThreadLocal基本使用及原理分析

ThreadLocal基本使用及原理分析

# ThreadLocal基本使用及原理分析 學習ThreadLocal的基本使用以及瞭解其核心原理實現。jdk版本:1.8 @[toc] ## ThreadLocal介紹 > ### 執行緒程式介紹 > > 早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal為解決多執行緒程式的併發問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優美的多執行緒程式。 > > > > ### 關於其變數 > > ThreadLocal很容易讓人望文生義,想當然地認為是一個“本地執行緒”。其實,ThreadLocal並不是一個Thread,而是Thread的[區域性變數](https://baike.baidu.com/item/%E5%B1%80%E9%83%A8%E5%8F%98%E9%87%8F),也許把它命名為ThreadLocalVariable更容易讓人理解一些。 > > 所以,在Java中編寫執行緒區域性變數的程式碼相對來說要笨拙一些,因此造成執行緒區域性變數沒有在Java開發者中得到很好的普及。 > > **-- 摘要自百度百科** ## ThreadLocal使用 在ThreadLocal中,提供了三個核心方法,get、set和remove。通過get賦值、通過set獲取值、通過remove刪除,使用起來還是非常簡單的。 ```java public void contextLoads() throws IOException { ThreadLocal threadLocal = new ThreadLocal<>(); threadLocal.set(1); System.out.println(threadLocal.get()); threadLocal.remove(); System.out.println(threadLocal.get()); System.in.read(); } ``` 接下來從原始碼的角度分析來了解ThreadLocal的核心原理。為什麼他是執行緒的區域性變數、怎麼做到執行緒獨佔的、會存在什麼問題。 ### set ```java public void set(T value) { //通過currentThread獲取到當前執行執行緒 Thread t = Thread.currentThread(); //這裡的ThreadLocalMap當成普通的hashMap來理解 ThreadLocalMap map = getMap(t); if (map != null)//不為null直接set值,為null則初始化 //key就是ThreadLocal物件本身 map.set(this, value); else createMap(t, value); } //初始化就是直接new了 void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } ``` set方法本身非常簡單,就是拿到當前執行緒的的ThreadLocalMap並賦值,因為key存的就是ThreadLocal物件本身,所以set方法不需要傳key。接下來在看一下get方法。 ### get ```java public T get() { //通過currentThread獲取到當前執行執行緒 Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) {//如果map是null的情況下做了一下初始化,否則從map中獲取值,值本身存放在map.Entry中 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); } //初始化操作 private T setInitialValue() { //初始化值就是一個null T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; } ``` get方法同樣簡單,未初始化的情況下線初始化返回null值,已經初始化的情況下從map中獲取值。這裡的獲取方式類似於1.8之前的hashMap,存放的是Entry陣列,通過ThreadLocal的hashcode & entry陣列的長度來拿到對應的下標並獲取值,後面在來分析這段。 ### remove ```java public void remove() { //同樣拿到threadLocalMap執行刪除方法 ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } ``` 三個方法分析完,可以得出,ThreadLocal就是拿到當前執行緒中的map來執行get、set和remove,key是ThreadLocal自身。可以發現貫穿流程的在於Thread的成員變數ThreadLocalMap,因此有必要了解一下關於ThreadLocalMap的實現。 ### ThreadLocalMap #### 定義 ```java static class ThreadLocalMap { /** * The entries in this hash map extend WeakReference, using * its main ref field as the key (which is always a * ThreadLocal object). Note that null keys (i.e. entry.get() * == null) mean that the key is no longer referenced, so the * entry can be expunged from table. Such entries are referred to * as "stale entries" in the code that follows. */ //注意,這裡的entry繼承了弱引用 static class Entry extends WeakReference> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal k, Object v) { //ThreadLocal key實際為entry的成員 super(k); value = v; } } /** * The initial capacity -- MUST be a power of two. */ private static final int INITIAL_CAPACITY = 16; /** * The table, resized as necessary. * table.length MUST always be a power of two. */ //資料儲存在entry陣列中 private Entry[] table; /** * The number of entries in the table. */ private int size = 0; /** * The next size value at which to resize. */ private int threshold; // Default to 0 } ``` ThreadLocalMap並沒有實現map的介面,自身是ThreadLocal的內部類,資料儲存在ThreadLocalMap的內部類Entry中,自身維護一個Entry陣列(型別1.8之前的hashMap)。在ThreadLocalMap中,是存在記憶體洩露的問題的,可以帶著這個問題來閱讀ThreadLocalMap的原始碼。 #### ThreadLocalMap.set ```java 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; //類似hashMap獲取下標的方式,但是這裡是hashCode不是通過hashCode()方法獲取的 int i = key.threadLocalHashCode & (len-1); //這裡是採用的開放定址法來解決的hash碰撞的問題。 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {//遍歷尋找到對應的entry並賦值 ThreadLocal k = e.get(); if (k == key) {//key相同(ThreadLocal相同)則直接賦值 e.value = value; return; } if (k == null) {//如果key為null //替換舊值 replaceStaleEntry(key, value, i); return; } } //如果tab[i]為null就直接建立一個entry並賦值了 tab[i] = new Entry(key, value); int sz = ++size; //cleanSomeSlots是為了清除為null的key,解決記憶體洩露的問題。 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } ``` set的程式碼邏輯參考如上註釋,可以看到,這裡的資料結構也是採用的散列表的結構,而關於ThreadLocal的hashcode,採用的**Fibonacci Hashing**,具體可以去了解一下斐波那契雜湊的相關概念。採用了開放定址法來解決hash碰撞的問題,因為這裡的entry是弱引用的實現,因此為了優化可能出現的記憶體洩露問題,在set、replaceStaleEntry、cleanSomeSlots等方法處都會去清理這些**stale key**。找到對應的entry後,將value賦值給entry.value。 #### ThreadLocalMap.getEntry ```java private Entry getEntry(ThreadLocal key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else //table[i]沒有找到或者不匹配的情況往後尋找 return getEntryAfterMiss(key, i, e); } ``` get的程式碼邏輯其實同set方法的尋找類似,看懂了set方法,get方法其實沒有什麼難度。 #### ThreadLocalMap.remove ```java /** * Remove the entry for key. */ 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) { //remove方法呼叫的時候,如果匹配到key的時候,清除stale的entry。 e.clear(); expungeStaleEntry(i); return; } } } ``` remove會主動清理stale的entry,因為entry是弱引用,所以在使用ThreadLocal的時候要主動去呼叫remove,這樣才能將對應的value移除,被GC回收。雖然在使用get、set方法時候也會cleanSomeSlots,但是需要觸發場景。 ### 記憶體洩露問題 分析完ThreadLocal和ThreadLocalMap的程式碼後,可以發現如果在使用ThreadLocal的時候,不主動呼叫remove方法時,可能會出現ThreadLocalMap中entry的value無法被GC回收的問題。雖然ThreadLocalMap是thread的成員變數,會隨著thread的銷燬而被回收,但是在日常開發中,我們往往會用到執行緒池,對於核心執行緒並不會被回收而是重複使用,導致thread一直存活且thread的成員變數ThreadLocalMap一直存活。因此在不用ThreadLocal後,一定記得呼叫remove銷燬。