ThreadLocal Java多線程下的影分身之術
如果寫過多線程的代碼,你肯定考慮過線程安全問題,更進一步你可能還考慮在在線程安全的前提下性能的問題。大多數情況下大家用來解決線程安全問題都會使用同步,比如用synchron或者concurrent包提供的各種鎖,當然這些都能解決問題。但有多線程做同步一定會涉及到資源爭搶和等待的問題。java中各種同步方法都是提供一種準入機制,JVM會調用系統同步原語來保證臨界區任意時刻只能有一個線程進入,那必然其他線程都得等待了,性能的瓶頸就在這同步上了。
解決問題最好的方式是啥?當然是避免問題的發生了。ThreadLocal就是用這樣一種方式提升性能的。ThreadLocal遍歷會為每個線程單獨維護一份值,某個線程對其做任何操作都不會影響其他的線程,這相當于這個對象在每個線程下面都有了一個分身。ThreadLocal是以Thread為維度實現的,所以多線程之間也不會有爭搶和等待,從而避免同步變成瓶頸,下文我們會從源碼的維度去看這些都是如何實現的。
ThreadLocal也不是萬金油,它也只能在多線程之間數據相互獨立的情況下使用,如果是多線程間的數據同步,還得使用某個同步的方式。 我的理解,ThreadLocal是在臨時變量完全不共享和全部變量完全共享之間取了個折中,在多線程數據一致的情況下完美的避免了資源爭搶和等待,提高了性能。
如何使用
ThreadLocal的使用也很簡單,直接new ThreadLocal
public class Demo extends Thread { private static ThreadLocal
上面我用到了ThreadLocal的set和get方法,其運行結果如下,因為使用了隨機數,可能每次運行解決會不一致。可以很明顯看得出,雖然多線對統一個Object操作,但卻沒有影響到各自的值。
Thread1:42 Thread0:20 Thread2:18 Thread3:6 Thread4:76 Thread5:50 Thread6:81 Thread7:75 Thread8:48 Thread9:56 Thread4:176 Thread7:175 Thread1:142 Thread6:181 Thread2:118 Thread5:150 Thread0:120 Thread3:106 Thread9:156
除了set和get接口外,ThreadLocal還提供了remove(),該方法可以將當前線程的所有內容清除掉。另外還有一個ThreadLocal withInitial()。
源碼分析
接下來我們就從源碼來剖析下ThreadLocal是如何實現不同線程下不同值的,首先我們來看下set()方法,這是我們在除了構造函數外第一個用的方法,它也承擔著ThreadLocal初始化的任務。
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { map.set(this, value); } else { createMap(t, value); } } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
set()也非常簡單,我順便也把set()涉及到的兩個方法貼上。set()首先獲取當前線程t,然后從t中獲取ThreadLocalMap,如果ThreadLocalMap為空就創建一個。ThreadLocalMap是ThreadLocal中比較核心的東西,稍后會詳細介紹。上面代碼很顯然,ThreadLocalMap是將ThreadLocal作為map的key。雖然多線程下都是用同一個ThreadLocal對象作為Key的,但每次獲取key對應的Value是從不同的Map中獲取,
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對象作為Key的,但每次獲取key對應的Value是從不同的Map中獲取,這就保證了多下次下value不會沖突。get方法在ThreadLocalMap未創建的情況下,還會調用setInitialValue()。
/** * Variant of set() to establish initialValue. Used instead * of set() in case user has overridden the set() method. * * @return the initial value */ 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); } if (this instanceof TerminatingThreadLocal) { TerminatingThreadLocal.register((TerminatingThreadLocal>) this); } return value; }
我總結看Java代碼的方法,就是先看類的聲明,然后按實際用途從每個方法入手看是怎么執行的。
static class ThreadLocalMap { } ``` ThreadLocalMap是直接聲明在ThreadLocal內部的,其他地方就沒法用了(其實外部也沒必要用,輪map的功能,它實現也沒有HashMap和Tree好)。另外,它沒有實現Map接口,emmm 這就意味它不是一個標準的map了。 ```java static class Entry extends WeakReference
ThreadLocalMap的Entry繼承了WeakReference,這讓我想到了WeakHashMap,這里用WeakReference的原因也很明確,就是想讓Key在失效后,Map能主動清理相關的Entry。
ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); } private void setThreshold(int len) { threshold = len * 2 / 3; }
ThreadLocalMap也有幾個默認參數,初始容量INITIAL_CAPACITY,threshold是容量的2/3,就是如果Map中的Entry數量超過總容量的2/3,ThreadLocalMap對進行擴容。
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 (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal> k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
從set方法中我們就可以看出ThreadLocalMap和HashMap,TreeMap的設計不同之處。首先也是對Key求hash值做定位,但當遇到hash沖突的時候,它的選擇不是開鏈,而是調用nextIndex往后移動,直到遇見某個entry為null或者其key和要插入的key一樣。同時,插入的過程也會調用replaceStaleEntry對Map做清理,清理過程比較復雜,我們稍后說。插入后,如果size大于閥值,也會對整個map做擴容操作。
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 return getEntryAfterMiss(key, i, e); } 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) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }
因為剛剛說到ThreadLocalMap處理key沖突的方式是往后移,直到有空閑的位置。這樣雖然實現簡單,但查的時候問題就來了,根據hash值算出來的位置沒有,并不意味著整個map里沒有,所以得往后遍歷,直到找到或者遍歷到某個空Entry。如果你仔細想想可能就會發現問題,如果只是遍歷到遇到null,而不是遍歷整個tab,可能會漏掉。比如下面這個例子。
| 0 | 1 | 2 | 3 | 5 | 6 | 7 | | | a | b | c | d | e | |
開始的時候,tab狀態是這樣的,現在我要插入一個h,其hashcode恰好是1,然而a已經在那了,按插入邏輯,h只能插到7的位置了,插入后如下。
| 0 | 1 | 2 | 3 | 5 | 6 | 7 | | | a | b | c | d | e | h |
后來,我把c刪掉,變成了下面這樣。如果我現在想查h,按照上面getEntry的邏輯,是不是遍歷到3就停了,所以找不到h了? getEntry的邏輯表面確實是這樣,但實際上getEntryAfterMiss、remove、gets時都會直接或者間接調用expungeStaleEntry會對表里的數據做整理。expungeStaleEntry()除了利用弱引用的特性對tab中Entry做清理外,還會對之前Hash沖突導致后移的Entry重新安放位置。所以不可能出現下面這種tab排放的。
| 0 | 1 | 2 | 3 | 5 | 6 | 7 | | | a | b | | d | e | h |
private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Rehash until we encounter null Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal> k = e.get(); if (k == null) { e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }
還有set中調用的replaceStaleEntry(),代碼很長,其實也是保證key失效的Entry被清理,Hash沖突的key能放回正確的位置。
private void replaceStaleEntry(ThreadLocal> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; // Back up to check for prior stale entry in current run. // We clean out whole runs at a time to avoid continual // incremental rehashing due to garbage collector freeing // up refs in bunches (i.e., whenever the collector runs). int slotToExpunge = staleSlot; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; // Find either the key or trailing null slot of run, whichever // occurs first for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal> k = e.get(); // If we find key, then we need to swap it // with the stale entry to maintain hash table order. // The newly stale slot, or any other stale slot // encountered above it, can then be sent to expungeStaleEntry // to remove or rehash all of the other entries in run. if (k == key) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // If we didn't find stale entry on backward scan, the // first stale entry seen while scanning for key is the // first still present in the run. if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // If key not found, put new entry in stale slot tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // If there are any other stale entries in run, expunge them if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
看這么多復雜的代碼,最后看個簡單的resize(),ThreadLocalMap的resize相較于HashMap的簡單多了,就是新建一個長度為當前2倍的tab,然后把當前tab中的每個entry重新計算index再插入新tab。
private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen]; int count = 0; for (Entry e : oldTab) { if (e != null) { ThreadLocal> k = e.get(); if (k == 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想要實現的功能和WeakHashMap類似,為什么不直接使用WeakHashMap呢!!
使用場景
數據庫連接
Cache
線程池
參考資料
ThreadLocal源碼
簡單理解ThreadLocal原理和適用場景,多數據源下ThreadLocal的應用
Java 任務調度 多線程 通用安全
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。