在多線程編程中一般解決線程安全的問題咱們會利用synchronzed或者lock控制線程對臨界區資源的同步順序從而解決線程安全的問題,可是這種加鎖的方式會讓未獲取到鎖的線程進行阻塞等待,很顯然這種方式的時間效率並非很好。線程安全問題的核心在於多個線程會對同一個臨界區共享資源進行操做,那麼,若是每一個線程都使用本身的「共享資源」,各自使用各自的,又互相不影響到彼此即讓多個線程間達到隔離的狀態,這樣就不會出現線程安全的問題。事實上,這就是一種「空間換時間」的方案,每一個線程都會都擁有本身的「共享資源」無疑內存會大不少,可是因爲不須要同步也就減小了線程可能存在的阻塞等待的狀況從而提升的時間效率。html
雖然ThreadLocal並不在java.util.concurrent包中而在java.lang包中,但我更傾向於把它看成是一種併發容器(雖然真正存放數據的是ThreadLoclMap)進行歸類。從ThreadLocal這個類名能夠顧名思義的進行理解,表示線程的「本地變量」,即每一個線程都擁有該變量副本,達到人手一份的效果,各用各的這樣就能夠避免共享資源的競爭。java
要想學習到ThreadLocal的實現原理,就必須瞭解它的幾個核心方法,包括怎樣存怎樣取等等,下面咱們一個個來看。git
void set(T value)github
set方法設置在當前線程中threadLocal變量的值,該方法的源碼爲:數據庫
public void set(T value) {
//1. 獲取當前線程實例對象
Thread t = Thread.currentThread();
//2. 經過當前線程實例獲取到ThreadLocalMap對象
ThreadLocalMap map = getMap(t);
if (map != null)
//3. 若是Map不爲null,則以當前threadLocl實例爲key,值爲value進行存入
map.set(this, value);
else
//4.map爲null,則新建ThreadLocalMap並存入value
createMap(t, value);
}
複製代碼
方法的邏輯很清晰,具體請看上面的註釋。經過源碼咱們知道value是存放在了ThreadLocalMap裏了,當前先把它理解爲一個普普統統的map便可,也就是說,數據value是真正的存放在了ThreadLocalMap這個容器中了,而且是以當前threadLocal實例爲key。先簡單的看下ThreadLocalMap是什麼,有個簡單的認識就好,下面會具體說的。編程
首先ThreadLocalMap是怎樣來的?源碼很清楚,是經過getMap(t)
進行獲取:數組
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
複製代碼
該方法直接返回的就是當前線程對象t的一個成員變量threadLocals:安全
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
複製代碼
也就是說ThreadLocalMap的引用是做爲Thread的一個成員變量,被Thread進行維護的。回過頭再來看看set方法,當map爲Null的時候會經過createMap(t,value)
方法:session
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
複製代碼
該方法就是new一個ThreadLocalMap實例對象,而後一樣以當前threadLocal實例做爲key,值爲value存放到threadLocalMap中,而後將當前線程對象的threadLocals賦值爲threadLocalMap。數據結構
如今來對set方法進行總結一下: 經過當前線程對象thread獲取該thread所維護的threadLocalMap,若threadLocalMap不爲null,則以threadLocal實例爲key,值爲value的鍵值對存入threadLocalMap,若threadLocalMap爲null的話,就新建threadLocalMap而後在以threadLocal爲鍵,值爲value的鍵值對存入便可。
T get()
get方法是獲取當前線程中threadLocal變量的值,一樣的仍是來看看源碼:
public T get() {
//1. 獲取當前線程的實例對象
Thread t = Thread.currentThread();
//2. 獲取當前線程的threadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//3. 獲取map中當前threadLocal實例爲key的值的entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//4. 當前entitiy不爲null的話,就返回相應的值value
T result = (T)e.value;
return result;
}
}
//5. 若map爲null或者entry爲null的話經過該方法初始化,並返回該方法返回的value
return setInitialValue();
}
複製代碼
弄懂了set方法的邏輯,看get方法只須要帶着逆向思惟去看就好,若是是那樣存的,反過來去拿就好。代碼邏輯請看註釋,另外,看下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;
}
複製代碼
這段方法的邏輯和set方法幾乎一致,另外值得關注的是initialValue方法:
protected T initialValue() {
return null;
}
複製代碼
這個方法是protected修飾的也就是說繼承ThreadLocal的子類可重寫該方法,實現賦值爲其餘的初始值。關於get方法來總結一下:
經過當前線程thread實例獲取到它所維護的threadLocalMap,而後以當前threadLocal實例爲key獲取該map中的鍵值對(Entry),若Entry不爲null則返回Entry的value。若是獲取threadLocalMap爲null或者Entry爲null的話,就以當前threadLocal爲Key,value爲null存入map後,並返回null。
void remove()
public void remove() {
//1. 獲取當前線程的threadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//2. 從map中刪除以當前threadLocal實例爲key的鍵值對
m.remove(this);
}
複製代碼
get,set方法實現了存數據和讀數據,咱們固然還得學會如何刪數據**。刪除數據固然是從map中刪除數據,先獲取與當前線程相關聯的threadLocalMap而後從map中刪除該threadLocal實例爲key的鍵值對便可**。
從上面的分析咱們已經知道,數據其實都放在了threadLocalMap中,threadLocal的get,set和remove方法實際上具體是經過threadLocalMap的getEntry,set和remove方法實現的。若是想真正全方位的弄懂threadLocal,勢必得在對threadLocalMap作一番理解。
ThreadLocalMap是threadLocal一個靜態內部類,和大多數容器同樣內部維護了一個數組,一樣的threadLocalMap內部維護了一個Entry類型的table數組。
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
複製代碼
經過註釋能夠看出,table數組的長度爲2的冪次方。接下來看下Entry是什麼:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
複製代碼
Entry是一個以ThreadLocal爲key,Object爲value的鍵值對,另外須要注意的是這裏的**threadLocal是弱引用,由於Entry繼承了WeakReference,在Entry的構造方法中,調用了super(k)方法就會將threadLocal實例包裝成一個WeakReferenece。**到這裏咱們能夠用一個圖(下圖來自http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/)來理解下thread,threadLocal,threadLocalMap,Entry之間的關係:
注意上圖中的實線表示強引用,虛線表示弱引用。如圖所示,每一個線程實例中能夠經過threadLocals獲取到threadLocalMap,而threadLocalMap實際上就是一個以threadLocal實例爲key,任意對象爲value的Entry數組。當咱們爲threadLocal變量賦值,實際上就是以當前threadLocal實例爲key,值爲value的Entry往這個threadLocalMap中存放。須要注意的是**Entry中的key是弱引用,當threadLocal外部強引用被置爲null(threadLocalInstance=null
),那麼系統 GC 的時候,根據可達性分析,這個threadLocal實例就沒有任何一條鏈路可以引用到它,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現key爲null的Entry,就沒有辦法訪問這些key爲null的Entry的value,若是當前線程再遲遲不結束的話,這些key爲null的Entry的value就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠沒法回收,形成內存泄漏。**固然,若是當前thread運行結束,threadLocal,threadLocalMap,Entry沒有引用鏈可達,在垃圾回收的時候都會被系統進行回收。在實際開發中,會使用線程池去維護線程的建立和複用,好比固定大小的線程池,線程爲了複用是不會主動結束的,因此,threadLocal的內存泄漏問題,是應該值得咱們思考和注意的問題,關於這個問題能夠看這篇文章----詳解threadLocal內存泄漏問題
與concurrentHashMap,hashMap等容器同樣,threadLocalMap也是採用散列表進行實現的。在瞭解set方法前,咱們先來回顧下關於散列表相關的知識(摘自這篇的threadLocalMap的講解部分以及這篇文章的hash)。
理想狀態下,散列表就是一個包含關鍵字的固定大小的數組,經過使用散列函數,將關鍵字映射到數組的不一樣位置。下面是
在理想狀態下,哈希函數能夠將關鍵字均勻的分散到數組的不一樣位置,不會出現兩個關鍵字散列值相同(假設關鍵字數量小於數組的大小)的狀況。可是在實際使用中,常常會出現多個關鍵字散列值相同的狀況(被映射到數組的同一個位置),咱們將這種狀況稱爲散列衝突。爲了解決散列衝突,主要採用下面兩種方式: 分離鏈表法(separate chaining)和開放定址法(open addressing)
分散鏈表法使用鏈表解決衝突,將散列值相同的元素都保存到一個鏈表中。當查詢的時候,首先找到元素所在的鏈表,而後遍歷鏈表查找對應的元素,典型實現爲hashMap,concurrentHashMap的拉鍊法。下面是一個示意圖:
圖片來自 http://faculty.cs.niu.edu/~freedman/340/340notes/340hash.htm
開放定址法不會建立鏈表,當關鍵字散列到的數組單元已經被另一個關鍵字佔用的時候,就會嘗試在數組中尋找其餘的單元,直到找到一個空的單元。探測數組空單元的方式有不少,這裏介紹一種最簡單的 -- 線性探測法。線性探測法就是從衝突的數組單元開始,依次日後搜索空單元,若是到數組尾部,再從頭開始搜索(環形查找)。以下圖所示:
圖片來自 http://alexyyek.github.io/2014/12/14/hashCollapse/
關於兩種方式的比較,能夠參考 這篇文章。ThreadLocalMap 中使用開放地址法來處理散列衝突,而 HashMap 中使用的分離鏈表法。之因此採用不一樣的方式主要是由於:在 ThreadLocalMap 中的散列值分散的十分均勻,不多會出現衝突。而且 ThreadLocalMap 常常須要清除無用的對象,使用純數組更加方便。
在瞭解這些相關知識後咱們再回過頭來看一下set方法。set方法的源碼爲:
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;
//根據threadLocal的hashCode肯定Entry應該存放的位置
int i = key.threadLocalHashCode & (len-1);
//採用開放地址法,hash衝突的時候使用線性探測
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//覆蓋舊Entry
if (k == key) {
e.value = value;
return;
}
//當key爲null時,說明threadLocal強引用已經被釋放掉,那麼就沒法
//再經過這個key獲取threadLocalMap中對應的entry,這裏就存在內存泄漏的可能性
if (k == null) {
//用當前插入的值替換掉這個key爲null的「髒」entry
replaceStaleEntry(key, value, i);
return;
}
}
//新建entry並插入table中i處
tab[i] = new Entry(key, value);
int sz = ++size;
//插入後再次清除一些key爲null的「髒」entry,若是大於閾值就須要擴容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
複製代碼
set方法的關鍵部分請看上面的註釋,主要有這樣幾點須要注意:
threadLocal的hashcode?
private final int threadLocalHashCode = nextHashCode();
private static final int HASH_INCREMENT = 0x61c88647;
private static AtomicInteger nextHashCode =new AtomicInteger();
/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
複製代碼
從源碼中咱們能夠清楚的看到threadLocal實例的hashCode是經過nextHashCode()方法實現的,該方法實際上老是用一個AtomicInteger加上0x61c88647來實現的。0x61c88647這個數是有特殊意義的,它可以保證hash表的每一個散列桶可以均勻的分佈,這是Fibonacci Hashing
,關於更多介紹能夠看這篇文章的threadLocal散列值部分。也正是可以均勻分佈,因此threadLocal選擇使用開放地址法來解決hash衝突的問題。
怎樣肯定新值插入到哈希表中的位置?
該操做源碼爲:key.threadLocalHashCode & (len-1)
,同hashMap和ConcurrentHashMap等容器的方式同樣,利用當前key(即threadLocal實例)的hashcode與哈希表大小相與,由於哈希表大小老是爲2的冪次方,因此相與等同於一個取模的過程,這樣就能夠經過Key分配到具體的哈希桶中去。而至於爲何取模要經過位與運算的緣由就是位運算的執行效率遠遠高於了取模運算。
怎樣解決hash衝突?
源碼中經過nextIndex(i, len)
方法解決hash衝突的問題,該方法爲((i + 1 < len) ? i + 1 : 0);
,也就是不斷日後線性探測,當到哈希表末尾的時候再從0開始,成環形。
怎樣解決「髒」Entry?
在分析threadLocal,threadLocalMap以及Entry的關係的時候,咱們已經知道使用threadLocal有可能存在內存泄漏(對象建立出來後,在以後的邏輯一直沒有使用該對象,可是垃圾回收器沒法回收這個部分的內存),在源碼中針對這種key爲null的Entry稱之爲「stale entry」,直譯爲不新鮮的entry,我把它理解爲「髒entry」,天然而然,Josh Bloch and Doug Lea大師考慮到了這種狀況,在set方法的for循環中尋找和當前Key相同的可覆蓋entry的過程當中經過replaceStaleEntry方法解決髒entry的問題。若是當前table[i]爲null的話,直接插入新entry後也會經過cleanSomeSlots來解決髒entry的問題,關於cleanSomeSlots和replaceStaleEntry方法,會在詳解threadLocal內存泄漏中講到,具體可看那篇文章
如何進行擴容?
threshold的肯定
也幾乎和大多數容器同樣,threadLocalMap會有擴容機制,那麼它的threshold又是怎樣肯定的了?
private int threshold; // Default to 0
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
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);
}
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
複製代碼
根據源碼可知,在第一次爲threadLocal進行賦值的時候會建立初始大小爲16的threadLocalMap,而且經過setThreshold方法設置threshold,其值爲當前哈希數組長度乘以(2/3),也就是說加載因子爲2/3(加載因子是衡量哈希表密集程度的一個參數,若是加載因子越大的話,說明哈希表被裝載的越多,出現hash衝突的可能性越大,反之,則被裝載的越少,出現hash衝突的可能性越小。同時若是太小,很顯然內存使用率不高,該值取值應該考慮到內存使用率和hash衝突機率的一個平衡,如hashMap,concurrentHashMap的加載因子都爲0.75)。這裏threadLocalMap初始大小爲16,加載因子爲2/3,因此哈希表可用大小爲:16*2/3=10,即哈希表可用容量爲10。
擴容resize
從set方法中能夠看出當hash表的size大於threshold的時候,會經過resize方法進行擴容。
/**
* Double the capacity of the table.
*/
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();
//遍歷過程當中若是遇到髒entry的話直接另value爲null,有助於value可以被回收
if (k == null) {
e.value = null; // Help the GC
} else {
//從新肯定entry在新數組的位置,而後進行插入
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
//設置新哈希表的threshHold和size屬性
setThreshold(newLen);
size = count;
table = newTab;
}
複製代碼
方法邏輯請看註釋,新建一個大小爲原來數組長度的兩倍的數組,而後遍歷舊數組中的entry並將其插入到新的hash數組中,主要注意的是,在擴容的過程當中針對髒entry的話會令value爲null,以便可以被垃圾回收器可以回收,解決隱藏的內存泄漏的問題。
getEntry方法源碼爲:
private Entry getEntry(ThreadLocal<?> key) {
//1. 肯定在散列數組中的位置
int i = key.threadLocalHashCode & (table.length - 1);
//2. 根據索引i獲取entry
Entry e = table[i];
//3. 知足條件則返回該entry
if (e != null && e.get() == key)
return e;
else
//4. 未查找到知足條件的entry,額外在作的處理
return getEntryAfterMiss(key, i, e);
}
複製代碼
方法邏輯很簡單,若能當前定位的entry的key和查找的key相同的話就直接返回這個entry,不然的話就是在set的時候存在hash衝突的狀況,須要經過getEntryAfterMiss作進一步處理。getEntryAfterMiss方法爲:
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)
//找到和查詢的key相同的entry則返回
return e;
if (k == null)
//解決髒entry的問題
expungeStaleEntry(i);
else
//繼續向後環形查找
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
複製代碼
這個方法一樣很好理解,經過nextIndex日後環形查找,若是找到和查詢的key相同的entry的話就直接返回,若是在查找過程當中遇到髒entry的話使用expungeStaleEntry方法進行處理。到目前爲止**,爲了解決潛在的內存泄漏的問題,在set,resize,getEntry這些地方都會對這些髒entry進行處理,可見爲了儘量解決這個問題幾乎無時無刻都在作出努力。**
/**
* 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) {
//將entry的key置爲null
e.clear();
//將該entry的value也置爲null
expungeStaleEntry(i);
return;
}
}
}
複製代碼
該方法邏輯很簡單,經過日後環形查找到與指定key相同的entry後,先經過clear方法將key置爲null後,使其轉換爲一個髒entry,而後調用expungeStaleEntry方法將其value置爲null,以便垃圾回收時可以清理,同時將table[i]置爲null。
ThreadLocal 不是用來解決共享對象的多線程訪問問題的,數據實質上是放在每一個thread實例引用的threadLocalMap,也就是說每一個不一樣的線程都擁有專屬於本身的數據容器(threadLocalMap),彼此不影響。所以threadLocal只適用於 共享對象會形成線程安全 的業務場景。好比hibernate中經過threadLocal管理Session就是一個典型的案例,不一樣的請求線程(用戶)擁有本身的session,若將session共享出去被多線程訪問,必然會帶來線程安全問題。下面,咱們本身來寫一個例子,SimpleDateFormat.parse方法會有線程安全的問題,咱們能夠嘗試使用threadLocal包裝SimpleDateFormat,將該實例不被多線程共享便可。
public class ThreadLocalDemo {
private static ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<>();
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
executorService.submit(new DateUtil("2019-11-25 09:00:" + i % 60));
}
}
static class DateUtil implements Runnable {
private String date;
public DateUtil(String date) {
this.date = date;
}
@Override
public void run() {
if (sdf.get() == null) {
sdf.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
} else {
try {
Date date = sdf.get().parse(this.date);
System.out.println(date);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
}
}
複製代碼
if (sdf.get() == null){....}else{.....}
能夠看出爲每個線程分配一個SimpleDateformat對象實例是從應用層面(業務代碼邏輯)去保證的。參考資料
《java高併發程序設計》 這篇文章的threadLocalMap講解和threadLocal的hashCode講解不錯 這篇文章講解了hash,不錯 解決hash衝突 鏈地址法和開放地址法的比較