ThreadLocal源碼分析

[toc]html

ThreadLocal源碼分析

簡單分析

什麼是ThreadLocal

ThreadLocal顧名思義能夠理解爲線程本地變量。也就是說若是定義了一個ThreadLocal,每一個線程往這個ThreadLocal中讀寫是線程隔離(也就是每一個線程讀寫都是本身的一份獨立對象,與其餘線程是無關的,固然前提是不一樣線程set的不是同一個對象的引用),互相之間不會影響的。它提供了一種將可變數據經過每一個線程有本身的獨立副本從而實現線程封閉的機制。java

大體實現思路

Thread類有一個類型爲ThreadLocal.ThreadLocalMap的實例變量threadLocals,也就是說每一個線程有一個本身的ThreadLocalMap。ThreadLocalMap有本身的獨立實現,能夠簡單地將它的key視做爲ThreadLocal,value爲代碼中放入的值(實際上key並非ThreadLocal自己,而是他的一我的弱引用)。每一個線程在往某個ThreadLocal裏賽值得時候,都會往本身的ThreadLocalMap裏存,讀也是以某個ThreadLocal做爲引用,在本身的map裏找對應的key,從而實現了線程隔離。web

使用場景

1.如在一個AccountService中寫了一段相似這樣的代碼:編程

Context ctx = new Context();
ctx.setTrackerID(.....)

而後這個AccountService 調用了其餘Java類,不知道通過了多少層調用之後,最終來到了一個叫作AccountUtil的地方,在這個類中須要使用Context中的trackerID來作點兒事情: 在這裏插入圖片描述 很明顯,這個AccountUtil沒有辦法拿到Context對象,怎麼辦? 1)能夠在調用每一個方法的時候,講Context對象一層一層往下傳遞。 存在的問題:會修改不少類的代碼,更重要的是有些類是第三方的根本沒有可能去修改源代碼。 2)講Context中的set/get方法改爲靜態的,而後再AccountUtil中直接Context.get調用便可api

public class Context{
    public static String getTrackerID(){
        ......
    }
    public static void setTrackerID(String id){
        ......
    }
}

這的確解決了一層一層傳遞的麻煩,可是出現了一個致命的問題。多線程併發問題!!!!!!!!!! 3)這個時候咱們能不能把這個值放到線程中?讓線程攜帶這個值,這樣咱們不管在任何地方均可以輕鬆獲取,且是線程安全的。也就是每一個線程都有一個私家領地。數組

public class Context {
    private static final ThreadLocal<String> mThreadLocal 
        = new ThreadLocal<String>();

    public static void setTrackerID(String id) {
        mThreadLocal.set(id); 
    }   
    public static String getTrackerID() {
        return mThreadLocal.get();
    }   
}

2.以session爲列理解ThreadLocal 在web開發的session中,不一樣的線程對應不一樣的session,那麼如何針對不一樣的線程獲取對應的session呢? 1)在action中建立session,而後傳遞給Service,Service再傳遞給Dao,很明顯,這種方式將代碼變得臃腫複雜。 2)使用一個類,SesssionFacoty裏面封裝getSeesion的靜態方法,而後在每一個Dao中SesssionFacoty.getSession()。存在併發訪問。 固然,對其方法加鎖,這樣的話效率上存在必定的問題。 3)建立一個靜態的map,鍵對應咱們的線程,值對應session,當咱們想獲取session時,只須要獲取map,而後根據當前的線程就能夠獲取對應的值。tomcat

private static final ThreadLocal<Session> threadSession = new ThreadLocal();
public static Session getSession() throws InterruptedException {
    Session s = threadSession.get();
    try {
        if (null == s){
            s = getSessionFactory.openSession();
            threadSession.set(s);
        }
    }catch (HibernateException e){
        throw  new InfrastructureExecption(e);
    }
    return s;
}

在Hibernate中經過使用ThreadLocal來實現的。在getSession方法中,若是ThreadLocal存在session,則返回session,不然建立一個session放入ThreadLocal中。安全

源碼分析

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.
 */
 //自帶一種基於弱引用的垃圾清理機制
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    //key爲ThreadLocal
    Entry(ThreadLocal<?> k, Object v) {
    	//ThreadLocal中存放的是ThreadLocal的弱引用
    	//這裏雖然使用弱引用在key能夠被回收,可是value還存在,因此不正確使用ThreadLocal可能會出現內存溢出
        super(k);
        value = v;
    }
}

爲何須要使用弱引用?

由於若是這裏使用普通的key-value形式來定義存儲結果,實質上就會形成節點的生命週期與線程強綁定,只要線程沒有銷燬,那麼節點在GC分析中一直處於可達狀態,那麼GC就沒辦回收,而程序自己也沒法判斷是否能夠清理節點。 弱引用是Java中四檔引用中的第三檔,比軟引用更加弱一些,若是一個對象沒有強引用鏈可達,那麼通常活不過下次GC。當某個ThreadLocal已經沒有強引用可達,則隨着它被垃圾回收,在ThreadLocalMap裏對應的Entry的鍵值會失效,這爲ThreadLocalMap自己的垃圾清理提供了便利。session

引用類型

1)強引用:就是指程序代碼中廣泛存在的,相似於「Object obj = new Object()」這類的引用,只要強引用還存在,垃圾回收器永遠不會回收掉被引用的對象,也就是說即便Java虛擬機內存空間不足時,GC收集器也毫不會回收該對象,若是內存空間不夠就會致使內存溢出。 2)軟引用:用來描述一些還有用,但並不是必需的對象。對於軟引用關聯着的對象,在系統要發生內存溢出異常以前,將會把這些對象列進回收範圍之中並進行回收,以避免出現內存溢出。若是此次回收仍是沒有足夠的內存,纔會拋出內存溢出異常。在 JDK 1.2 以後,提供了 SoftReference 類來實現軟引用。 2)弱引用:也就是用來描述非必需對象的,可是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生以前。當垃圾回收器工做時,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。在 JDK 1.2 以後,提供了 WeakReference 類來實現弱引用。ThreadLocal使用到的就有弱引用。 4)虛引用:也稱爲幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的惟一目的就是但願能在這個對象被收集器回收時收到一個系統通知。在 JDK 1.2 以後,提供了PhantomReference 類來實現虛引用。 在這裏插入圖片描述多線程

ThreadLocal內存溢出

ThreadLocalMap使用ThreadLocal的弱引用做爲key,若是一個ThreadLocal沒有外部強引用來引用它,那麼系統 GC 的時候,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現key爲null的Entry,就沒有辦法訪問這些key爲null的Entry的value,若是當前線程再遲遲不結束的話,這些key爲null的Entry的value會一直存在一條強引用鏈:Thread Ref → Thread → ThreaLocalMap → Entry → value永遠沒法回收,形成內存泄漏。 其實,ThreadLocalMap的設計中已經考慮到這種狀況,也加上了一些防禦措施:在ThreadLocal的get(),set(),remove()的時候都會清除線程ThreadLocalMap裏全部key爲null的value。

可是這些被動的預防措施並不能保證不會內存泄漏。 1)使用static的ThreadLocal,延長了ThreadLocal的生命週期,可能致使的內存泄漏 2)分配使用了ThreadLocal又再也不調用get(),set(),remove()方法,那麼就有可能會致使內存泄漏。

其實ThreadLocal是否會引發內存泄漏也是一個比較有爭議性的問題。 認爲ThreadLocal會引發內存泄漏的說法是由於若是一個ThreadLocal對象被回收了,咱們往裏面放的value對於【當前線程->當前線程的threadLocals(ThreadLocal.ThreadLocalMap對象)->Entry數組->某個entry.value】這樣一條強引用鏈是可達的,所以value不會被回收。 認爲ThreadLocal不會引發內存泄漏的說法是由於ThreadLocal.ThreadLocalMap源碼實現中自帶一套自我清理的機制。 之因此有關於內存泄露的討論是由於在有線程複用如線程池的場景中,一個線程的壽命很長,大對象長期不被回收影響系統運行效率與安全。若是線程不會複用,用完即銷燬了也不會有ThreadLocal引起內存泄露的問題。《Effective Java》一書中的第6條對這種內存泄露稱爲unintentional object retention(無心識的對象保留)。 當咱們仔細讀過ThreadLocalMap的源碼,咱們能夠推斷,若是在使用的ThreadLocal的過程當中,顯式地進行remove是個很好的編碼習慣,這樣是不會引發內存泄漏。 那麼若是沒有顯式地進行remove呢?只能說若是對應線程以後調用ThreadLocal的get和set方法都有很高的機率會順便清理掉無效對象,斷開value強引用,從而大對象被收集器回收。 但不管如何,咱們應該考慮到什麼時候調用ThreadLocal的remove方法。一個比較熟悉的場景就是對於一個請求一個線程的server如tomcat,在代碼中對web api做一個切面,存放一些如用戶名等用戶信息,在鏈接點方法結束後,再顯式調用remove。

類成員變量與相應方法

/**
 * The initial capacity -- MUST be a power of two.
 * 初始容量,必須爲2的冪
 */
 private static final int INITIAL_CAPACITY = 16;
 /**
  * Entry表,大小爲2的冪
  * 對於2的冪做爲模數取模,能夠用&(2^n-1)來替代%2^n,位運算比取模效率高不少。
  * 至於爲何,由於對2^n取模,只要不是低n位對結果的貢獻顯然都是0,會影響結果的只能是低n位。
  */
 private Entry[] table;
 /**
  * 表裏entry的個數
  */
 private int size = 0;
 /**
  * 從新分配表大小的闕值,默認爲0
  */
 private int threshold; // Default to 0
 //色湖之resize闕值以維持最壞的2/3的裝載因子
 private void setThreshold(int len) {
   threshold = len * 2 / 3;
}
/**
* 使得Entry造成一個環形。因此它這裏並非像HashMap中,是一個數組,而後每一個數組中下面掛上一個鏈路(java8是二叉樹),
* 在這裏它使用的是線性探測法,即若是hash索引到某個index,產生衝突會往下遍歷尋找下一個。
*/
private static int nextIndex(int i, int len) {
  return ((i + 1 < len) ? i + 1 : 0);
}
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}
/**
* 構造一個包含firstKey和firstValue的map
* ThreadLocalMap是惰性構造的,因此只有當至少要往裏面放一個元素的時候纔會構建它。
*/
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);
}

getEntry方法

這個方法會被ThreadLocal的get方法直接調用,用於獲取map中某個ThreadLocal存放的值。

private Entry getEntry(ThreadLocal<?> key) {
    // 根據key這個ThreadLocal的ID來獲取索引,也即哈希值
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // 對應的entry存在且未失效且弱引用指向的ThreadLocal就是key,則命中返回
    if (e != null && e.get() == key) {
        return e;
    } else {
        // 由於用的是線性探測,因此日後找仍是有可能可以找到目標Entry的。
        return getEntryAfterMiss(key, i, e);
    }
}
/*
 * 調用getEntry未直接命中的時候調用此方法
 */
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    // 基於線性探測法(一個一個往下找)不斷向後探測直到遇到空entry。
    while (e != null) {
        ThreadLocal<?> k = e.get();
        // 找到目標
        if (k == key) {
            return e;
        }
        if (k == null) {
            // 該entry對應的ThreadLocal已經被回收,調用expungeStaleEntry來清理無效的entry
            expungeStaleEntry(i);
        } else {
            // 環形意義下日後面走,
            i = nextIndex(i, len);
        }
        e = tab[i];
    }
    return null;
}
/**
 * 這個函數是ThreadLocal中核心清理函數,它作的事情很簡單:
 * 就是從staleSlot開始遍歷,將無效(弱引用指向對象被回收)清理,即對應entry中的value置爲null,將指向這個entry的table[i]置爲null,直到掃到空entry。
 * 另外,在過程當中還會對非空的entry做rehash。
 * 能夠說這個函數的做用就是從staleSlot開始清理連續段中的slot(斷開強引用,rehash slot等)
 */
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    // 由於entry對應的ThreadLocal已經被回收,value設爲null,顯式斷開強引用
    tab[staleSlot].value = null;
    // 顯式設置該entry爲null,以便垃圾回收
    tab[staleSlot] = null;
    size--;
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        // 清理對應ThreadLocal已經被回收的entry
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            /*
             * 對於尚未被回收的狀況,須要作一次rehash。
             * 若是對應的ThreadLocal的ID對len取模出來的索引h不爲當前位置i,
             * 則從h向後線性探測到第一個空的slot,把當前的entry給挪過去。
             */
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null; 
                while (tab[h] != null) {
                    h = nextIndex(h, len);
                }
                tab[h] = e;
            }
        }
    }
    // 返回staleSlot以後第一個空的slot索引
    return i;
}

總結: 1)根據入參threadLocal的threadLocalHashCode對錶容量取模獲得index 2)若是index對應的solt就是要讀的threadLoacl,則直接返回結果 3)調用getEntryAfterMiss線性探測,過程當中每碰到無效slot,調用expungeStaleEntry進行段清理(清理只是再get沒有獲得的狀況下才有可能發生);若是找到了key,則返回結果entry 4)沒有找到key,返回null

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();
        // 找到對應的entry
        if (k == key) {
            e.value = value;
            return;
        }
        // 替換失效的entry
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold) {
        rehash();
    }
}

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;
    // 向前掃描,查找最前的一個無效slot
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len)) {
        if (e.get() == null) {
            slotToExpunge = i;
        }
    }
    // 向後遍歷table
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        // 找到了key,將其與無效的slot交換
        if (k == key) {
            // 更新對應slot的value值
            e.value = value;
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;
            /*
             * 若是在整個掃描過程當中(包括函數一開始的向前掃描與i以前的向後掃描)
             * 找到了以前的無效slot則以那個位置做爲清理的起點,
             * 不然則以當前的i做爲清理起點
             */
            if (slotToExpunge == staleSlot) {
                slotToExpunge = i;
            }
            // 從slotToExpunge開始作一次連續段的清理,再作一次啓發式清理
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // 若是當前的slot已經無效,而且向前掃描過程當中沒有無效slot,則更新slotToExpunge爲當前位置
        if (k == null && slotToExpunge == staleSlot) {
            slotToExpunge = i;
        }
    }

    // 若是key在table中不存在,則在原地放一個便可
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // 在探測過程當中若是發現任何無效slot,則作一次清理(連續段清理+啓發式清理)
    if (slotToExpunge != staleSlot) {
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    }
}

/**
 * 啓發式地清理slot,
 * i對應entry是非無效(指向的ThreadLocal沒被回收,或者entry自己爲空)
 * n是用於控制控制掃描次數的
 * 正常狀況下若是log n次掃描沒有發現無效slot,函數就結束了
 * 可是若是發現了無效的slot,將n置爲table的長度len,作一次連續段的清理
 * 再從下一個空的slot開始繼續掃描
 * 
 * 這個函數有兩處地方會被調用,一處是插入的時候可能會被調用,另外個是在替換無效slot的時候可能會被調用,
 * 區別是前者傳入的n爲元素個數,後者爲table的容量
 */
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        // i在任何狀況下本身都不會是一個無效slot,因此從下一個開始判斷
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            // 擴大掃描控制因子
            n = len;
            removed = true;
            // 清理一個連續段
            i = expungeStaleEntry(i);
        }
    } while ((n >>>= 1) != 0);
    return removed;
}

private void rehash() {
    // 作一次全量清理
    expungeStaleEntries();
    /*
     * 由於作了一次清理,因此size極可能會變小。
     * ThreadLocalMap這裏的實現是調低閾值來判斷是否須要擴容,
     * threshold默認爲len*2/3,因此這裏的threshold - threshold / 4至關於len/2
     */
    if (size >= threshold - threshold / 4) {
        resize();
    }
}

/*
 * 作一次全量清理
 */
private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        if (e != null && e.get() == null) {
            /*
             * 我的以爲這裏能夠取返回值,若是大於j的話取了用,這樣也是可行的。
             * 由於expungeStaleEntry執行過程當中是把連續段內全部無效slot都清理了一遍了。
             */
            expungeStaleEntry(j);
        }
    }
}

/**
 * 擴容,由於須要保證table的容量len爲2的冪,因此擴容即擴大2倍
 */
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    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();
            if (k == null) {
                e.value = null; 
            } else {
                // 線性探測來存放Entry
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null) {
                    h = nextIndex(h, newLen);
                }
                newTab[h] = e;
                count++;
            }
        }
    }
    setThreshold(newLen);
    size = count;
    table = newTab;
}

總結: 1)探測過程當中slot都不無效,而且順利找到key所在的slot,直接替換便可 2)在探測過程當中發現無效solt,調用replaceStableEntry,效果是最終必定會把key和value放在這個slot,而且會盡量清理無效slot

  • 在replaceStaleEntry過程當中,若是找到了key,則作一個swap把它放到那個無效slot中,value置爲新值
  • 在replaceStaleEntry過程當中,沒有找到key,直接在無效slot原地放entry 3)探測沒有發現key,則在連續段末尾的後一個空位置放上entry,這也是線性探測法的一部分。放完後,作一次啓發式清理,若是沒清理出去key,而且當前table大小已經超過閾值了,則作一次rehash,rehash函數會調用一次全量清理slot方法也即expungeStaleEntries,若是完了以後table大小超過了threshold - threshold / 4,則進行擴容2倍

remove方法

/**
 * 從map中刪除ThreadLocal
 */
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) {
            // 顯式斷開弱引用
            e.clear();
            // 進行段清理
            expungeStaleEntry(i);
            return;
        }
    }
}

ThreadLocal

Thread、ThreadLocal,ThreadLocalMap關係

在這裏插入圖片描述 一個Thread中只有一個ThreadLocalMap(Thread中有一個成員變量ThreadLocal.ThreadLocalMap threadLocals = null;),一個ThreadLocalMap中能夠有多個ThreadLoacl對象,其中一個ThreadLocal對象對應一個ThreadLocalMap中一個的Entry實體。

ThreadLoacl源碼分析

public void set(T value) {
   //獲取調用改set方法的線程
   Thread t = Thread.currentThread();
   //根據線程去獲取ThreadLocalMap,若是還爲初始化,那麼就調用createMap建立初始化,構造直接map.set(this,value)便可
    ThreadLocalMap map = getMap(t);
    if (map != null)
    	//直接調用ThreadLocalMap的set方法
        map.set(this, value);
    else
    	//線程第一次set,初始化ThreadLocalMap
        createMap(t, value);
}
//初始化Tread中的ThreadLocal.ThreadLocalMap threadLocals
void createMap(Thread t, T firstValue) {
   t.threadLocals = new ThreadLocalMap(this, firstValue);
}

public T get() {
    Thread t = Thread.currentThread();
    //獲取該線程的ThreadLoalMap對象
    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;
        }
    }
    //若是ThreadLocalMap還未被初始化,就返回默認值
    return setInitialValue();
}

//Removes the current thread's value for this thread-local variable. 
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

參考博客

一個故事講明白ThreadLocal:https://mp.weixin.qq.com/s/aM03vvSpDpvwOdaJ8u3Zgw ThreadLocal源碼解讀:http://www.javashuo.com/article/p-mksbvkvj-dm.html Java多線程編程-(18)-借ThreadLocal出現OOM內存溢出問題再談弱引用WeakReference:https://blog.csdn.net/xlgen157387/article/details/78513735?ref=myread

相關文章
相關標籤/搜索