從源碼看Thread&ThreadLocal&ThreadLocalMap的關系與原理


1.三者的之間的關系

ThreadLocalMap是Thread類的成員變量threadLocals,一個線程擁有一個ThreadLocalMap,一個ThreadLocalMap可以有多個ThreadLocal。

ThreadLocalMap是ThreadLocal的內部類,ThreadLocal的set(),get(),remove()方法其實都是對ThreadLocalMap的操作。ThreadLocalMap中是以內部類Entry的形式關聯ThreadLocal和對應的Value,其中Entry對ThreadLocal為弱引用(WeakReference<>).

如下圖,大概描述了下三者的關系

 

 

2: 結構分析

首先看下Thread類,可以看到有個ThreadLocalMap類型的成員變量threadLocals,之后所有針對當前線程的ThreadLocal的存取,都是該變量來操作。

public class Thread implements Runnable {
 /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

 

再來看下ThreadLocalMap的結構,它是ThreadLocal的內部類

 static class ThreadLocalMap {
//內部類Entry繼承了弱引用()
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */
//通過ThreLocal.set()保存的值 Object value;
//構造函數 Entry(ThreadLocal
<?> k, Object v) {
//調用WeakReference的構造方法,實現Entry對ThreadLocal的弱引用
super(k); value = v; } } /** * 初始容量,即table的初始化大小 */ private static final int INITIAL_CAPACITY = 16; /** * Entry數組,用來保存每一個ThreadLocal */ private Entry[] table; /** * 當前table中實際存放的Entry的數量 */ private int size = 0; /** * 擴容閾值,默認為0 */ private int threshold; // Default to 0 /** * Set the resize threshold to maintain at worst a 2/3 load factor.
設置擴容閾值的方法,可以看到ThreadLocalMap中的擴容的負載因子為2/3
*/ private void setThreshold(int len) { threshold = len * 2 / 3; }

3.完整流程分析

正常情況下我們使用ThreadLocal來存取變量都是這樣的

        ThreadLocal<String> test = new ThreadLocal<>();
        test.set("111");

 

首先看下ThreadLocal.set(T value)方法

    public void set(T value) {
//獲取當前線程 Thread t
= Thread.currentThread();
//根據線程獲取ThreLocalMap,其實就是獲取Thread的成員變量 ThreadLocalMap map
= getMap(t);
//如果map!=null,則則將當前ThreadLocal進行設置
if (map != null) map.set(this, value); else
//map==null,則對該線程的ThreadLocalMap進行初始化 createMap(t, value); }


ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

 

當前線程第一次使用ThreadLocal, createMap()方法初始化ThreadLocalMap

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }


        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//初始化table,初始化大小為16 table
= new Entry[INITIAL_CAPACITY];
//計算插入的數組下標,將threadLocael的hashcode與15進行按位與操作
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//將新構建的Entry放到計算的數組下標上 table[i]
= new Entry(firstKey, firstValue);
//table中實際長度賦值1 size
= 1;
//設置擴容閾值,這個方法我們上面看到過,內部算法就是initial_capacity * 2/3 setThreshold(INITIAL_CAPACITY); }

 

ThreadLocalMap已經存在,再次添加ThreadLocal

        private void set(ThreadLocal<?> key, Object value) {
            //獲取當前table
            Entry[] tab = table;
            int len = tab.length;
//計算出數組插入下標
int i = key.threadLocalHashCode & (len-1); //從計算出的下標位置i開始遍歷table數組,直到下一個元素Entry為null時停止
//這里解決Hash沖突的方法采用的線性探測法,計算出的位置有值的話就相鄰的向下一直探索直到有位置
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); //判斷當前遍歷的ThreadLocale是否和添加進來的key相等 if (k == key) {
//更新value e.value
= value; return; } //如果存在Entry中ThreadLocal為null的情況,即該線程變量已過時,則對過時的Entry進行清除 if (k == null) { replaceStaleEntry(key, value, i); return; } } //走到這里說明目前的table中不存在該ThreadLocale,則創建新Entry放到計算的下標處 tab[i] = new Entry(key, value);
//table實際長度+1
int sz = ++size;
//if(!快速遍歷一遍table判斷是否存在Entry中ThreadLocal為null的情況&&當前table的實際長度>=擴容閾值) 則進行擴容
if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }

 

看完了ThreadLocal的set()方法,再來看get()方法

    public T get() {
//獲取當前線程 Thread t
= Thread.currentThread();
//獲取當前線程持有的ThreadLocalMap ThreadLocalMap map
= getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked")
//返回的Entry!=null的話直接返回其儲存的value值 T result
= (T)e.value; return result; } }
//如果ThreadLocalMap==null或者找不到該Entry,返回設置的默認值
return setInitialValue(); }

 

ThreadLocalMap!=null時調用getEntry()方法

        private Entry getEntry(ThreadLocal<?> key) {
//計算出在table中的數組下標
int i = key.threadLocalHashCode & (table.length - 1);
//獲取指定下標中的E Entry e
= table[i];
//如果Entry!=&&ThreadLocal==當前的ThreadLocale,直接返回該Entry
if (e != null && e.get() == key) return e; else
//找到的元素不對或者位置上沒有元素 return getEntryAfterMiss(key, i, e); } private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
//獲取當前table Entry[] tab
= table;
//獲取table長度
int len = tab.length; while (e != null) {
//如果Entry!=null,就取出來再判斷一下ThreadLocal是否相同 ThreadLocal
<?> k = e.get(); if (k == key) return e; if (k == null)
//清除掉key已失效的E expungeStaleEntry(i);
else //以當前的數組下標下后遍歷Entry,因為set()時插入Entry發生Hash沖突時用的是線性探測法解決的,所以get()查找時也按此原則
i
= nextIndex(i, len); e = tab[i]; }
//如果遍歷完table都找不到,返回null
return null; }

 

get()獲取時ThreadLocalMap還為空時調用的初始化方法setInitialValue()方法

    private T setInitialValue() {
//獲取初始化value,該方法內部直接返回的為null T value
= initialValue();
//獲取當前線程 Thread t
= Thread.currentThread();
//獲取該線程的ThreadLocalMap ThreadLocalMap map
= getMap(t);
//如果map!=null,則用初始化的值來添加
if (map != null) map.set(this, value); else
//如果map==null,則用這個初始化值null和當前的這個ThreadLocal來創建ThreadLocalMap進行初始化
createMap(t, value); return value; }

 

使用完ThreadLocal,最好清除下remove()

     public void remove() {
//獲取當前線程的ThreadLocalMap ThreadLocalMap m
= getMap(Thread.currentThread()); if (m != null)
map!=null就進行刪除 m.remove(
this); } private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1);
//獲取到下標后線性探測法遍歷table,找到后進行刪除
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } }

 

remove()調用的關聯方法:

    public void clear() {
//將Entry內部的弱引用的ThreadLocal置為null,方便下一次GC時進行對ThreadLocal對象進行回收
this.referent = null; }
//釋放table中的Entry
private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; //將Entry的value的引用置為null,此時Entry不再持有任何引用,ThreadLocal和value的引用都已清除, // expunge entry at staleSlot tab[staleSlot].value = null;
//將該位置的Entry的引用置為null,此時此Entry也不再被table強引用,下次GC時也會回收 tab[staleSlot]
= null;
//table實際長度-1 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; }

 

4.ThreadLocal內存泄漏問題分析

通過上述的源碼我們ThreadLocal的使用及原理有了大致的了解,那么在使用ThreadLocal的同時很大可能會出現內存泄漏問題,下面我們來探討下這究竟是怎么回事,圖來源於網絡

 

 

當一個Thread使用完ThreadLocal存儲變量完,對應的ThreadLocal的引用被清除,這時候該ThreadLocal的強引用被清除,但是Thread的ThreadLocalMap中的Entry的key還存在着ThreadLocal的弱引用,當發生Young GC時該弱引用就會被清除,這時就會存在Entry中key=null,這導致該ThreadLocalMap永遠訪問不到該value,value就會內存泄漏,除非ThreadLocalMap對象也被清除。

這是由於Threrd和ThreadLocalMap的生命周期一樣長,如果該在ThreadLocal清除后該Thread一直存活,那么就一直存在着value內存泄漏的問題。

 

既然使用了對ThreadLocal的弱引用出現了Entry中value的內存泄露,那為什么還要使用弱引用呢?如果變成強引用呢?

我們來看下,如果Entry中變成強引用ThreadLocal, 當外部的ThreadLocal強引用被清除后,由於Entry內部還有強引用,但外部又無法再通過ThreadLocal訪問到,就會導致Entry的內存泄漏,泄漏對象變的更大,並且GC回收時也不會回收該Entry對象。

針對該內存泄漏現象,官方也做了相應的處理,我們在上面的源碼中可以看到,不管是在調用ThreadLocal的set(),get()還是remove()方法每次在調用時遍歷table的時候會因為hash沖突向下遍歷一段距離,這遍歷過程中如果有發現Entry中ThreadLocal為null的情況,會進行處理,將Entry完全清除掉,但是這個遍歷的范圍非常有限,很有可能遍歷不到為null的那個Entry,即使set()方法在第一次插入ThreadLocal時還會進行一次快速的遍歷table,但終究不是完全遍歷,所以通過官方的優化,內存泄漏的問題還是不能夠很好的解決。

內存泄漏的問題我們使用規范的話,完全是可以避免的:

1.在每次使用完ThreadLocal時,使用ThreadLocal.remove()方法,這樣就會清除調Entry中的key和value的引用。

2.將ThreadLocal對象設置為private static 變成共享對象,讓所有線程都使用該ThreadLocal對象,這樣ThreadLocal就一直存在外部強引用,GC時就不會清除Entry的ThreadLocal,不出出現內存泄漏,但是加大了內存開銷,盡量還是使用完就使用remove()進行處理。

 

另外一提:

因為線程池中的線程會存在復用,所以可以能存在讀出臟數據的問題。即當線程池中某個線程使用ThreadLocal存儲數據時,使用過后沒有remove,等下次從線程池調用到該線程的時候,就會讀到該線程上一次執行任務時的數據。所以務必需要remove()。

 

ps: 由於筆者水平有限,可能存在一些地方理解不正確,希望大家能夠指出。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM