ThreadLocal源碼深度解析與應用案例

  本文詳細介紹了Java中的ThreadLocal的作用、原理、源碼以及應用,並且介紹了ThreadLocal的內存泄漏的原理以及解決辦法。

1 ThreadLocal的概述

1.1 ThreadLocal的入門

public class ThreadLocal< T >
  extends Object

  ThreadLocal來自JDK1.2,位於java.lang包。ThreadLocal可以提供線程內的局部變量,這種變量在線程的生命週期內起作用,ThreadLocal又叫做線程本地變量/線程本地存儲。
  實際上,單就ThreadLocal這個類來說,它不存儲任何內容,真正存儲數據的集合在每個Thread中的threadLocals變量裏面,ThreadLocal中只是定義了這個集合的結構,並提供了一系列操作的方法。後面的源碼分析處會講到!
  可以說,ThreadLocal只是一個工具類,一個對各個線程的threadLocals進行操作的工具而已。
ThreadLocal 的作用和目的:

  1. 用於實現線程內的數據共享,某些數據是以線程爲作用域並且不同線程具有不同的數據副本時,即數據在線程之間隔離,就可以考慮用ThreadLocal。即對於相同的程序代碼,多個模塊在同一個線程中運行時要共享一份數據,而在另外線程中運行時又共享另外一份數據。
  2. 方便同一個線程複雜邏輯下的數據傳遞,有些時候一個線程中的任務過於複雜,我們又需要某個數據能夠貫穿整個線程的執行過程,可能涉及到不同類/函數之間數據的傳遞。此時使用Threadlocal存放數據,在線程內部只要通過get方法就可以獲取到在該線程中存進去的數據,方便快捷。

1.2 同步和ThreadLocal

  1. 同步與ThreadLocal是解決多線程中數據訪問問題的兩種思路,前者是數據共享的思路,後者是數據隔離的思路。同步是一種以時間換空間的思想,ThreadLocal是一種空間換時間的思想。前者僅提供一份變量,讓不同的線程排隊訪問,實現串行化;而後者爲每一個線程都提供了一份變量,因此可以同時訪問而互不影響。
  2. Threadlocal並不能代替同步,注意ThreadLocal不是用來解決共享對象的多線程訪問問題的。通過ThreadLocal的set()方法設置到線程的threadLocals裏的是線程自己要存儲的對象,其他線程不需要去訪問,也是訪問不到的。各個線程中的threadLocals以及裏面的值都是不同的對象。Threadloocal是用來進行變量隔離,就是說ThreadLocal是針對那些不需要共享的屬性!

1.3 主要API方法與使用案例

ThreadLocal類主要有四個可供調用的方法:

  1. void set(T value):保存值;
  2. T get():獲取值;
  3. void remove():移除值;
  4. initialValue():返回該線程局部變量的初始值,該方法是爲了讓子類繼承而設計的。這個方法是一個延遲調用方法,在一個線程第一次調用get()時(並且set未被調用)才執行。ThreadLocal中的默認實現是直接返回一個null。

ThreadLocal實現線程內數據共享,線程間數據隔離的案例:

public class ThreadLocalTest {
    /**
     * 全局ThreadLocal對象位於堆中,這是線程共享的,而方法棧,是每個線程私有的
     */
    static ThreadLocal<String> th = new ThreadLocal<>();

    public static void set() {
        //設置值,值爲當前線程的名字
        th.set(Thread.currentThread().getName());
    }

    public static String get() {
        //獲取值
        return th.get();
    }


    public static void main(String[] args) throws InterruptedException {
        System.out.println("主線程中嘗試獲取值:" + get());
        //主線程中設置值,值爲線程名字
        set();
        //主線程中嘗試獲取值
        System.out.println("主線程中再次嘗試獲取值:" + get());
        //開啓一條子線程
        Thread thread = new Thread(new Th1(), "child");
        thread.start();
        //主線程等待子線程執行完畢
        thread.join();
        System.out.println("等待子線程執行完畢,主線程中再次嘗試獲取值:" + get());
    }

    static class Th1 implements Runnable {
        @Override
        public void run() {
            System.out.println("子線程中嘗試獲取值:" + get());
            //子線程中設置值,值爲線程名字
            set();
            System.out.println("子線程中再次嘗試獲取值:" + get());
        }
    }
}

結果如下:

主線程中嘗試獲取值:null
主線程中再次嘗試獲取值:main
子線程中嘗試獲取值:null
子線程中再次嘗試獲取值:child
等待子線程執行完畢,主線程中再次嘗試獲取值:main

  先設置值,然後獲取,可以得到“main”。然後開啓子線程,在子線程內部,先獲取,得到null,然後設置值,再獲取,得到“child”。最後在主線程中再嘗試獲取,得到的還是原值“main”,這說明ThreadLocal使得變量的作用範圍限制在本線程中了,其他線程是無法訪問到該變量的。
  注意這裏由於案例演示在最後並沒有調用remove方法,在實際使用中應該在使用完畢之後調用remove方法,原理後面會講!

2 ThreadLocal的原理

2.1 基本關係

  ThreadLocal類中定義了一個內部類ThreadLocalMap,ThreadLocalMap是真正存放數據的容器,實際上它的底層就是一張哈希表。
  每個Thread線程內部都定義有一個ThreadLocal.ThreadLocalMap類型的threadLocals變量,這樣,線程之間的ThreadLocalMap互不干擾。threadLocals變量持有的ThreadLocalMap在ThreadLocal調用set或者get方法時纔會初始化。
  ThreadLocal還提供相關方法,負責向當前線程的ThreadLocalMap變量獲取和設置線程的變量值,相當於一個工具類。
  當在某個線程的方法中使用ThreadLocal設置值的時候,就會將該ThreadLocal對象添加到該線程內部的ThreadLocalMap中,其中鍵就是該ThreadLocal對象,值可以是任意類型任意值。當在某個線程的方法中使用ThreadLocal獲取值的時候,會以該ThreadLocal對象爲鍵,在該線程的ThreadLocalMap中獲取對應的值。
類結構:
  ThreadLocal中定義了ThreadLocalMap的結構,並提供操作的方法。

    public class ThreadLocal<T> {
        //……
        static class ThreadLocalMap {
            //……
        }

        /**
         * ThreadLocal的構造器,裏面什麼都沒有
         * 創建ThreadLocal時,沒有初始化ThreadLocalMap,在set、get方法中還可能初始化!
         */
        public ThreadLocal() {
        }
    }

  每個thread對象都持有一個ThreadLocalMap類型的引用變量,用於存放線程本地變量。key爲ThreadLocal對象,value爲要存儲的數據。

public class Thread implements Runnable {
    /*與此線程相關的線程本地值。此ThreadLocalMap定義在ThreadLocal類中,使用在Thread類中*/
    ThreadLocal.ThreadLocalMap threadLocals = null;
    //………………
}

  下面是Thread、threadlocalMap、ThreadLocal的關係圖:
在這裏插入圖片描述

2.2 基本結構

  ThreadLocal中定義了ThreadLocalMap的結構。
  ThreadLocalMap也是一張key-value類型的哈希表,但是ThreadLocalMap並沒有實現Map接口,它內部具有一個Entry類型的table數組用於存放節點。Entry節點用於存放key、value數據,並且繼承了WeakReference。
  通過對該ThreadLocal對象進行哈希運算,可以得到該ThreadLocal對象在Entry數組中的桶位,從而找到唯一的Entry。如果發生了哈希衝突,那麼與HashMapHashtable採用的“鏈地址法”不同,ThreadLocalMap採用“線性探測法”解決哈希衝突,採用該方法的原因是實現很簡單,佔用更小的空間,並且一般來說一個ThreadLocalMap並不會存放很多數據!關於哈希表的原理以及各種解決哈希衝突的方法詳解,可以看這篇文章:數據結構—散列表(哈希表)的原理以及Java代碼的實現
  在創建ThreadLocalMap對象的同時即初始化16個長度的內部table數組,擴容閾值爲len * 2 / 3,擴容增量爲增加原容量的1倍。在沒有使用ThreadLocal設置、獲取值時,線程中的ThreadLocalMap對象一直爲null。

/**
 * ThreadLocal的內部類ThreadLocalMap
 */
static class ThreadLocalMap {

    /**
     * table數組的初始化容量,
     */
    private static final int INITIAL_CAPACITY = 16;
    //存放數據的數組,在創建ThreadLocalMap對象時將會初始化該數組,大小必須是2^N次方
    private Entry[] table;
    //擴容閾值,爲len * 2 / 3
    private int threshold;

    /**
     * 內部節點對象,貌似沒找到“key”字段在哪裏,實際上存放k的屬性位於其父類WeakReference的父類Reference中,名爲referent,即屬性複用
     * 插入數據時,通過對key(threadLocal對象)的hash計算,來找出Entry應該存放的table數組的桶位,
     * 不過可能造成hash衝突,它採用線性探測法解決衝突,因此需要線性向後查找。
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        //存放值
        Object value;
        //構造器
        Entry(ThreadLocal<?> k, Object v) {
            //調用父類的構造器,傳入key,這裏k被包裝成爲弱引用
            //實際上存放k的屬性位於其父類WeakReference的父類Reference中,名爲referent,即屬性複用
            super(k);
            value = v;
        }
    }
}

2.3 set方法

  set方法是由ThreadLocal提供的,用於存放數據,大概步驟如下:

  1. 獲取當前線程的成員變量threadLocals;
  2. 如果threadLocals不等於null,則調用set方法存放數據,方法結束;
  3. 否則,調用createMap方法初始化threadLocals,然後存放數據,方法結束。
/**
 * ThreadLocal中的方法,開放給外部調用的存放數據的方法
 *
 * @param value 需要存放的數據
 */
public void set(T value) {
    //注意,這裏首先獲取當前線程t
    Thread t = Thread.currentThread();
    //1.1 然後通過getMap方法,傳入t,獲取當前t線程的threadLocals
    ThreadLocalMap map = getMap(t);
    //如果map存在,則存放數據
    if (map != null)
        //this代指當前ThreadLocal對象,value表示值
        map.set(this, value);
    else
        //如果不存在,則構建屬於當前線程的ThreadLocalMap並存放數據
        createMap(t, value);
}

/**
 * 1.1 ThreadLocal中的方法,獲取指定線程的threadLocals
 *
 * @param t 指定線程
 * @return t的threadLocals
 */
ThreadLocalMap getMap(Thread t) {
    //t代表當前線程,獲取該線程的threadLocals屬性,該屬性就是一個ThreadLocalMap,默認爲null
    return t.threadLocals;
}


/**
 * 1.2 ThreadLocal中的方法,用於構建ThreadLocalMap對象並賦值
 *
 * @param t          當前線程
 * @param firstValue 要存入的值
 */
void createMap(Thread t, T firstValue) {
    //該方法是ThreadLocal中的方法,this代指當前ThreadLocal對象
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}


/**
 * 位於ThreadLocalMap中的構造器,用於創建新的ThreadLocalMap對象
 *
 * @param firstKey   key
 * @param firstValue value
 */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    //創建table數組,初始容量爲INITIAL_CAPACITY,即16
    table = new Entry[INITIAL_CAPACITY];
    //尋找數組桶位,通過ThreadLocal對象的threadLocalHashCode屬性 & 15
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    //該位置存放元素,由於是剛創建對象,因此不存在哈希衝突的情況,直接存儲就行了
    //構造器在“基本結構”部分分析過,key最終被包裝成弱引用。
    table[i] = new Entry(firstKey, firstValue);
    //size設置爲1
    size = 1;
    //setThreshold方法設置擴容閥值
    setThreshold(INITIAL_CAPACITY);
}

/**
 * ThreadLocalMap中的方法,設置擴容閾值
 *
 * @param len 數組長度
 */
private void setThreshold(int len) {
    //數組長度的2/3
    threshold = len * 2 / 3;
}

2.3.1 內部set方法

  上面的set方法中,如果當前t線程的threadLocals不爲null,那麼又調用了另一個私有的set方法存放數據。該方法是ThreadLocal的核心方法之一,並且比較複雜,大概具有如下步驟:

  1. 通過哈希算法計算出當前key存放的桶位i,並獲取i的元素e。
  2. 如果e不爲空,說明發生哈希衝突,使用線性探測法替換或者存放數據:
    a) 如果e的key和指定key相等(使用==比較),那麼替換value,方法結束;
    b) 否則,如果e的key等於null,那說明是無效數據。調用replaceStaleEntry從該索引開始清理無效數據,並且存放新數據,在replaceStaleEntry過程中:
      i. 如果找到了key相等的entry,則它放到無效桶位中,value置爲新值,方法結束。
      ii. 如果沒找到key相等的entry,直接在無效slot原地放entry,方法結束。
      iii.調用到了replaceStaleEntry方法,那就肯定能將新數據存入ThreadLocalMap中,並且不再執行後續步驟。
    c) 否則,nextIndex方法獲取下一個索引並賦值給i,如果該位置的節點e爲null,則結束循環,否則進行下一次循環;
  3. 走到這一步,說明沒有替換value,也沒有沒有進行無效數據清理,而是找到了一個空桶位i,直接在該位置插入新entry,此時肯定保證最初始的i和現在的之間的位置都是存在有效節點的;
  4. 存放元素完畢之後,再調用cleanSomeSlots做一次部分無效節點清理,如果沒清理出去key(返回false)並且當前table大小 大於等於 閾值,則調用rehash方法;
  5. rehash方法中會調用一次全表掃描清理的方法即expungeStaleEntries()方法。如果expungeStaleEntries完畢之後table大小還是大於等於(threshold – threshold / 4),則調用resize方法進行擴容;
  6. resize方法將擴容兩倍,同時完成節點的轉移。

  ThreadLocalMap使用==比較key是否相同。ThreadLocalMap解決Hash衝突的方式就是簡單的步長加1或減1(線性探測),尋找下一個相鄰的位置。當向前尋找到數組頭部或者向後尋找到數組尾部的時候,下一個位置就是數組尾部或者數組頭部,即循環查找。

/**
 * 位於ThreadLocalMap內的set方法,用於存放數據。
 *
 * @param key   ThreadLocal對象
 * @param value 值
 */
private void set(ThreadLocal<?> key, Object value) {
    //tab保存數組引用
    Entry[] tab = table;
    //len保存數組的度
    int len = tab.length;
    /*1 哈希算法計算桶位   通過ThreadLocal的threadLocalHashCode屬性計算出該key(ThreadLocal對象)對應的數組桶位i*/
    int i = key.threadLocalHashCode & (len - 1);
    /*
     *  2 使用線性探測法存放元素,可能進行垃圾清理
     * 獲取i索引位置的Entry e,如果e不爲null,說明發生了哈希衝突,下面開始解決:
     * 判斷兩個key是否相等,即是否需要進行value替換,如果相等,則替換value,解決完畢,方法返回;
     * 否則,判斷獲取的e的key是否爲null,如果獲取的ThreadLocal爲null,這說明該WeakReference(弱引用)被回收了(因爲Entry繼承了WeakReference),
     * 說明ThreadLocal肯定在外部沒有強引用了,這個Entry變成了垃圾,調用replaceStaleEntry方法擦除該位置或者其他的無效的Entry,重新賦值,解決完畢,方法返回,這是爲了防止內存泄漏
     * 否則判斷該位置i是否爲null,即沒有節點,如果爲null,則在該位置新建節點並插入,解決完畢。
     * 否則,i = nextIndex(i, len),嘗試下一次循環。
     * 如果循環完畢,方法還沒結束,那說明沒找到key相等的節點和key==null的節點,但是找到了下一個節點爲null的桶位,記錄此時索引值i,將會在該位置插入新節點。
     *
     * 這就是ThreadLocalMap解決哈希衝突的辦法,即開放定址法——線性探測:當衝突時,向下查找下一個節點爲null的位置存放新節點
     * nextIndex()方法用於循環數組索引,即如果初始i爲15,長度爲16,那麼nextIndex將返回0,如果初始i爲1,長度爲16,那麼nextIndex將返回2。
     * 這樣的做法有利於利用起始索引前面的空間
     * */
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        /*獲取該Entry的key,即原來的ThreadLocal對象,這是其父類Reference的方法*/
        ThreadLocal<?> k = e.get();
        /*如果獲取的ThreadLocal和要存的ThreadLocal是同一個對象,那麼就替換值,方法結束
         * 這裏能夠看出,判斷key相等的條件是兩個對象使用==比較返回true
         * */
        if (k == key) {
            e.value = value;
            return;
        }
        /*
         *如果獲取的ThreadLocal爲null,這說明該WeakReference(弱引用)被回收了(因爲Entry繼承了WeakReference),
         * 說明ThreadLocal肯定在外部沒有強引用了,這個Entry變成了垃圾,擦除該位置的Entry,重新賦值並結束方法,這是爲了防止內存泄漏*/
        if (k == null) {
            /*
             * 從該位置開始,繼續尋找key,並且會盡可能清理其他無效slot
             * 在replaceStaleEntry過程中,如果找到了key,則做一個swap把它放到那個無效slot中,value置爲新值
             * 在replaceStaleEntry過程中,沒有找到key,直接在該無效slot原地放entry
             * */
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    /*
     * 執行到這一步方法還沒有返回,說明i位置沒有節點,此時e等於null,直接在該位置插入新的Entry
     * 此時肯定保證最初始的i和現在的之間的位置是存在節點的!
     * */
    tab[i] = new Entry(key, value);
    //size自增1,使用sz記錄
    int sz = ++size;
    /* 3 嘗試清理垃圾,然後判斷是否需要擴容,如果需要那就擴容
     * 存放完畢元素之後,再調用cleanSomeSlots做一次垃圾清理,如果沒清理出去key(返回false)
     * 並且當前table大小大於等於閾值,則調用rehash方法
     * rehash方法中會調用一次全量清理slot方法也即expungeStaleEntries()方法
     * 如果expungeStaleEntries完畢之後table大小還是大於等於(threshold – threshold / 4),則調用resize方法進行擴容
     * resize方法將擴容兩倍,同時完成節點的轉移
     * */
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}


/**
 * 在length的索引範圍內獲取i的下一個索引,循環
 */
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

/**
 * 在length的索引範圍內獲取i的上一個索引,循環
 */
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

private void rehash() {
    expungeStaleEntries();
    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();
}

2.3.2 哈希算法

  在set方法中,我們可以找到ThreadLocalMap的哈希算法爲:

int i = key.threadLocalHashCode & (len - 1);

  由於len長度一定是2的冪次方,因此上面的位運算可以轉換爲key.threadLocalHashCode% len,所以說ThreadLocalMap的哈希算法也是一種取模(求餘)算法,因爲餘數一定會比除數小,那麼計算出來的桶位肯定是位於[0, len-1]之間了,剛好在底層數組的索引範圍內,還是比較簡單的。
  這裏的key我們知道是ThreadLocal對象,這個threadLocalHashCode屬性看名字猜測就是該對象的哈希值了,那麼這個值是通過hashCode方法得到的嗎?實際上,threadLocalHashCode這個屬性的得來非常的有趣,我們必須要去ThreadLocal源碼中去看看!

public class ThreadLocal<T> {
    /**
     * 下一個hashCode
     * 注意:這是個靜態屬性,那麼只有在ThreadLocal的類第一次被加載進行類初始化的時候會被初始化,明顯,初始化時爲0。
     */
    private static AtomicInteger nextHashCode = new AtomicInteger();

    /**
     * threadlocal對象的hashcode,並非通過HashCode方法得到,他有自己的計算規則
     * 可以看到,它是調用nextHashCode()方法的返回值得來的
     */
    private final int threadLocalHashCode = nextHashCode();

    /**
     * 每個threadLocal對象通過該方法獲取自己的hashcode
     */
    private static int nextHashCode() {
        //內部使用nextHashCode對象的getAndAdd方法
        //該方法首先返回當前的值,然後使得當前值的值加上指定的值,這裏是HASH_INCREMENT
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

    /**
     * 哈希增量,顧名思義,就是哈希值的增量
     */
    private static final int HASH_INCREMENT = 0x61c88647;
}

  結合上面的幾個屬性和方法,我們終於明白:
  在第一次創建ThreadLocal實例時,會加載ThreadLocal類,此時nextHashCode初始化值爲0,然後是該對象threadLocalHashCode屬性的初始化,在創建該類對象完畢之後,會自動調用nextHashCode方法,將此時nextHashCode的值作爲自己的hashCode並且nextHashCode對象的值增加HASH_INCREMENT,明顯是作爲下一個ThreadLocal實例的hashCode值。
  即,每一個ThreadLocal實例使用創建該實例時的nextHashCode值作爲自己的hashCode,然後將nextHashCode值增加HASH_INCREMENT,作爲下一個ThreadLocal實例的hashCode。
  0x61c88647是十六進制的數,轉換爲十進制就是1640531527,實際上這個哈希增量的值的選取和斐波那契散列法、黃金比例有關(https://www.javaspecialists.eu/archive/Issue164.html),目的是爲了讓哈希碼能更加均勻的分佈在2的N次方的數組裏。

2.4 get方法

  對於不同的線程,每次獲取變量值時,是從本線程內部的threadLocals中獲取的,別的線程並不能獲取到當前線程的值,形成了變量的隔離,互不干擾。大概步驟如下:

  1. 獲取當前線程的成員變量threadLocals
  2. 如果threadLocals非空,調用getEntry方法嘗試查找並返回節點e:
    a) 如果e不爲null,說明找到了,那愛麼返回e的value,方法結束
    b) 如果e爲null,說明沒找到,方法繼續。
  3. 到這一步,說明可能是threadLocals爲空,或者沒找到e。那麼調用setInitialValue方法,以當前ThreadLocal對象爲key設置一個entry,並返回value。
/**
 * ThreadLocal中的get方法,開放給外部調用
 *
 * @return 當前線程的當前ThreadLocal對象存入的值
 */
public T get() {
    //獲取當前線程
    Thread t = Thread.currentThread();
    //獲取當前線程的threadLocals對象
    ThreadLocalMap map = getMap(t);
    //如果map不爲null,即表示已經初始化過
    if (map != null) {
        //從map獲取對應的Entry節點,傳入this代表當前的ThreadLocal對象
        ThreadLocalMap.Entry e = map.getEntry(this);
        //如果e不爲null
        if (e != null) {
            //獲取並返回值
            T result = (T) e.value;
            return result;
        }
    }
    //否則,如果map爲null,或者e爲null
    //那麼返回null或者自定義的初始值
    return setInitialValue();
}

2.4.1 getEntry方法

  ThreadLocalMap內部的方法,根據key,嘗試獲取對應的Entry節點。 大概步驟如下:

  1. 根據key計算出桶位;
  2. 獲取該桶位節點e,如果e不爲null並且e的key和指定key相等(使用==比較),那麼返回e,方法結束;
  3. 否則,調用getEntryAfterMiss方法進行一個步長的線性探測查找,查找過程中每碰到無效的節點,調用expungeStaleEntry進行清理;如果找到了則返回找到的entry;沒有找到(探測到了空的桶位),則返回null。
/**
 * ThreadLocalMap內部的方法,根據key,獲取對應的Entry節點
 *
 * @param key key
 * @return Entry節點,沒找到就返回null
 */
private Entry getEntry(ThreadLocal<?> key) {
    //根據key計算桶位
    int i = key.threadLocalHashCode & (table.length - 1);
    //獲取Entry節點e
    Entry e = table[i];
    /*如果e不爲nul並且並且e內部key等於當前key(ThreadLocal對象)*/
    //可以看到key相等是使用==直接比較的
    if (e != null && e.get() == key)
        //則返回e
        return e;
    else
        /*否則使用線性探測查找
        線性探測查找過程中每碰到無效slot,調用expungeStaleEntry進行清理;如果找到了則返回entry;沒有找到,返回null*/
        return getEntryAfterMiss(key, i, e);
}

2.4.2 setInitialValue方法

  ThreadLocal的方法,用於設置並返回初始值,在get方法沒有找key對應的節點時,會調用該方法! 大概有如下幾步:

  1. 獲取initialValue方法的返回值,作爲新節點的value;
  2. 獲取當前線程的ThreadLocalMap,判斷是否爲null;
  3. 如果不爲null,則以當前ThreadLocal對象爲key,存放value,方法結束;
  4. 如果爲null,則初始化此線程的ThreadLocalMap,並以當前ThreadLocal對象爲key,存放value,方法結束。
/**
 * ThreadLocal的方法,設置並返回初始值
 *
 * @return 返回null或者通過initialValue方法用戶自定義的初始值
 */
private T setInitialValue() {
    //返回null或者用戶重寫該方法時自定義的返回值
    T value = initialValue();
    //獲取當前線程
    Thread t = Thread.currentThread();
    //獲取當前線程的map
    ThreadLocal.ThreadLocalMap map = getMap(t);
    //如果map不爲null
    if (map != null)
        //嘗試添加節點,以當前ThreadLocal對象爲key,以null或者自定義的初始值爲value
        map.set(this, value);
    else
        //否則初始化map並設置值
        createMap(t, value);
    //返回value,null或者自定義的初始值
    return value;
}

2.4.2.1 initialValue方法

  當get方法沒有找到數據時,會調用setInitialValue方法,該方法中會調用initialValue方法,將默認返回null,用戶也可以重寫該方法,用於返回指定的值,相當於默認初始值。
  setInitialValue將會以當前ThreadLocal對象爲key,initialValue的返回值爲value,存放一個節點,同時返回value的值。

/**
 * ThreadLocal的方法,默認返回空
 *
 * @return 默認返回null, 用戶可以重寫該方法返回自定義的初始值
 */
protected T initialValue() {
    return null;
}

2.4.2.2 默認初始值案例

public class ThreadLocalInitialValue {
    public static void main(String[] args) {
        //th1覆寫了initialValue方法
        ThreadLocal th1 = new ThreadLocal() {
            @Override
            protected Object initialValue() {
                return 11;
            }
        };
        //th2沒有覆寫了initialValue方法
        ThreadLocal th2 = new ThreadLocal();
        //由於並沒有調用set方法設置數據,那麼兩個ThreadLocal的get方法都將不能找到存放的數據
        //此時th1將返回默認初始值,並設置key:th1 value:11
        System.out.println("th1初始值:" + th1.get());
        //th2將返回null,並設置key:th2 value:null
        System.out.println("th2初始值:" + th2.get());
    }
}

2.5 ThreadLocal的內存泄露

2.5.1 內存泄漏的原理

  首先是基礎知識,關於Java中的引用的介紹:Java中強、軟、弱、虛四種對象引用的詳解和案例演示
  根據上面的源碼,我們知道在存放新節點時在Entry節點的構造器中,並不是直接使用ThreadLocal對象作爲key的,而是使用由ThreadLocal對象包裝成的弱引用對象作爲Key的。
  爲什麼使用弱引用包裝的ThreadLocal對象作爲key?因爲如果某個entry直接使使用一個普通屬性和ThreadLocal對象關聯,即key時強引用。那麼當最外面ThreadLocal對象的全局變量引用置空時,由於在ThreadLocalMap中存在key對這個ThreadLocal對象的強引用,那麼這個ThreadLocal對象並不會被回收,但此時我們已經無法訪問、利用這個對象,造成了key的內存泄漏。
  因此,ThreadLocal對象被包裝爲弱引用作爲key。這樣,當外部的ThreadLocal對象的強引用被清除時,由於在ThreadLocalMap中存儲的是弱引用key,那麼下一次GC時這個弱引用可以被清除,這個ThreadLocal對象也就可以被清除了。但是仍然會造成內存泄漏,不過此時是value的內存泄漏。
  我們知道value是強引用。這就導致了一個問題,如果這個弱引用key被回收而變成null時,如果之前調用ThreadLocal方法設置值的線程一直持續運行,那麼它的ThreadLocalMap也一直存在,那麼內部的entry節點也一直存在,那麼value肯定還存在,但是此時卻不能通過key訪問到了(因爲key被回收變成null了),此時還是發生了內存泄露。

2.5.2 如何避免內存泄漏

  我們在set和get方法的源碼中能夠看到,當遍歷的entry的key爲null時,此時將清除該entry,value置空,這樣就可以解決部分內存泄漏問題。但這並不是絕對的,可能並沒有遍歷到key爲null的entry時set、get方法就因爲插入、獲取成功而返回了,因此在set、get方法中,只會嘗試將遍歷的到無效數據清除,並且這種方式是一種被動的清除,不能即時清除無效數據。
  ThreadLocal還有一個remove方法,該方法可以將此ThreadLocal對象對應的entry清除。實際上,在對ThreadLocal的數據使用完畢之後,從邏輯上來說此時的entry就是無效的數據了,因此主動調用一次remove方法,將該entry移除。這樣我們對使用完畢的entry進行手動清除,從根本上杜絕了內存泄漏問題。
  所以養成良好的編程習慣十分重要,使用完ThreadLocal的數據之後,一定要記得調用一次remove方法。

3 總結與應用

總結
  每個ThreadLocal由於實現線程本地存儲,但是隻能保存一個本地數據,如果想要一個線程能夠保存多個數據,就需要創建多個ThreadLocal。
  ThreadLocalMap的key鍵爲ThreadLocal包裝成的弱引用,會有內存泄漏的風險,因此使用完畢必須手動調用remove清除!
應用
  使用ThreadLocal的典型場景正如上面的數據庫連接管理,線程會話管理等場景,只適用於獨立變量副本的情況,如果變量爲全局共享的,則不適用在高併發下使用。
  spring對於有狀態的單例Bean,比如RequestContextHolder。將它們在多線程下可能發生線程安全問題的屬性使用ThreadLocal封裝,這樣每條線程還是訪問同一份對象,但是使用的對象中的數據卻是線程私有的。
  原始的JDBC方式的時候可以使用ThreadLocal類來管理事務!

如果有什麼不懂或者需要交流,可以留言。另外希望點贊、收藏、關注,我將不間斷更新各種Java學習博客!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章