【Java多線程】ThreadLocal 原理分析與使用場景

一.線程隔離

當多線程訪問時,通過將數據封閉在各自的線程中相互隔離,互不干擾的技術稱爲線程隔離ThreadLocal就是線程隔離的一種體現

二.ThreadLocal是什麼

  • ThreadLocal類提供了一種線程局部變量(ThreadLocal),即每一個線程都會保存一份變量副本,每個線程都可以獨立地修改自己的變量副本,而不會影響到其他線程
  • ThreadLocal 變量通常被private static修飾,其中保存變量屬於當前線程,該變量對其他線程而言是隔離的,當一個線程結束時,它所使用的所有 ThreadLocal 實例副本都可被回收。
  • ThreadLocal 適用於變量在線程間隔離而在方法或類間共享的場景。
  • ThreadLocal唯一的缺點就是:只能用於存儲當前線程的變量。子線程獲取不到父線程的數據(使用InheritableThreadLocals`可以解決)

三.ThreadLocal類提供的方法

方法 描述
public T get() 獲取ThreadLocal在當前線程中保存的變量副本
public void set(T value) 設置當前線程中變量的副本
public void remove() 移除當前線程中變量的副本
protected T initialValue() initialValue()是一個protected方法,一般是用來在使用時進行重寫的,它是一個延遲加載方法, 返回此線程局部變量當前副本中的初始值

ThreadLocalMap是ThreadLocal的靜態內部類,該類纔是實現線程隔離機制的關鍵。,get()、set()、remove()方法底層都是對該內部類進行操作,ThreadLocalMap用鍵值對方式存儲每個線程變量的副本,key當前ThreadLocal對象value對應線程的變量副本

四.入門使用

假設每個線程都需要一個計數器記錄自己做某件事做了多少次,各線程運行時都需要改變自己的計數值而且相互不影響,那麼ThreadLocal就是很好的選擇,這裏ThreadLocal裏保存的當前線程的局部變量的副本就是這個計數值。

public class SeqCount {

  private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>(){
      @Override
      protected Integer initialValue() {//初始化值爲0
          return 0;
      }
  };

    //當前統計 + 1,然後返回
    public int nextSeq() {
        seqCount.set(seqCount.get() +1);
        return seqCount.get();
    }

    public static void main(String [] args) {
        SeqCount seqCount = new SeqCount();

        //開啓四個線程
        SeqThread seqThread1 = new SeqThread(seqCount);
        SeqThread seqThread2 = new SeqThread(seqCount);
        SeqThread seqThread3 = new SeqThread(seqCount);
        SeqThread seqThread4 = new SeqThread(seqCount);

        seqThread1.start();
        seqThread2.start();
        seqThread3.start();
        seqThread4.start();
    }
    
    //靜態內部類,循環調用3次seqCount.nextSeq()方法
    public static class SeqThread extends Thread {

        private SeqCount seqCount;

        public SeqThread(SeqCount seqCount) {
            this.seqCount = seqCount;
        }

        @Override
        public void run() {
            for (int i=0; i<3; i++) {
                System.out.println(Thread.currentThread().getName()+" seqCount:"+seqCount.nextSeq());
            }
        }
    }
}

執行結果:
在這裏插入圖片描述

五.ThreadLocal

1.ThreadLocal的數據結構

在這裏插入圖片描述

  1. Thread類有一個類型爲ThreadLocal.ThreadLocalMap的實例變量threadLocals,也就是說每個線程有一個自己的ThreadLocalMap。

  2. ThreadLocalMap有自己的獨立實現,可以簡單地將它的key視作ThreadLocal,value爲代碼中放入的值(實際上key並不是ThreadLocal本身,而是它的一個弱引用)。

  3. 每個線程在往ThreadLocal裏放值的時候,都會往自己的ThreadLocalMap裏存,讀也是以ThreadLocal作爲引用,在自己的ThreadLocalMap裏找對應的key,從而實現了線程隔離。

  4. ThreadLocalMap類似於HashMap的結構,只是·HashMap是由數組+鏈表(數組組+鏈表+紅黑樹結構,當鏈表長度大於8,轉爲紅黑樹)實現的,而ThreadLocalMap中並沒有鏈表結構,使用Entry來保存鍵值對, 它的key是ThreadLocal<?> k,繼承自WeakReference, 也就是我們常說的弱引用類型,在發生GC時會被回收。

1.1.Java的四種引用類型

Java有四種引用類型,引用強度從強到弱依次爲:強引用、軟引用、弱引用和虛引用

  • 強引用:我們常常new出來的對象就是強引用類型,只要強引用存在,垃圾回收器將永遠不會回收被引用的對象,哪怕內存不足的時候

  • 軟引用:使用SoftReference修飾的對象被稱爲軟引用,軟引用指向的對象在內存要溢出的時候被回收

  • 弱引用:使用WeakReference修飾的對象被稱爲弱引用,只要發生垃圾回收,若這個對象只被弱引用指向,那麼就會被回收

  • 虛引用:虛引用是最弱的引用,在 Java 中使用 PhantomReference 進行定義。虛引用中唯一的作用就是用隊列接收對象即將死亡的通知

1.2.GC之後,Entry的key是否是null?

ThreadLocal 是弱引用,那麼在threadLocal.get()的時候,發生GC之後,key是否是null?

我們使用反射的方式來看看GC當前線程的ThreadLocalMap的數據情況:

public class ThreadLocalDemo {

    public static void main(String[] args) throws InterruptedException {
        //開啓線程1並調用test()方法
        Thread test1 = new Thread(() -> test("11111", false),"test1");
        test1.start();
        //當前線程t等待主線程執行完在執行
        test1.join();

        System.out.println("--gc後--");

        //開啓線程1並調用test()方法且調用GC()
        Thread test2 = new Thread(() -> test("22222", true),"test2");
        test2.start();
        //當前線程t2等待主線程執行完在執行
        test2.join();
    }

	 /**
     * 測試GC和非GC時當前線程的ThreadLocal是否被回收
     * @param s 字符串
     * @param isGC 是否調用GC true調用/false不調用
     */
    private static void test(String s, boolean isGC) {
        try {
            //當前線程設置ThreadLocal
            new ThreadLocal<>().set(s);
            
            //發起GC
            if (isGC) {
                System.gc();
            }
            // 獲取當前線程
            Thread currentThread = Thread.currentThread();
            // 獲取當前線程的class
            Class<? extends Thread> clazz = currentThread.getClass();
            // 獲取當前線程的threadLocals屬性對象
            Field field = clazz.getDeclaredField("threadLocals");
            // 設置threadLocals字段的可見級別
            field.setAccessible(true);
            //獲取當前線程對象的threadLocals的屬性值
            Object threadLocalMap = field.get(currentThread);

            // 獲取當前線程 threadLocals屬性的class(ThreadLocal.ThreadLocalMap)
            Class<?> tlmClass = threadLocalMap.getClass();
            // 獲取當前線程ThreadLocal.ThreadLocalMap類內的table屬性對象
            Field tableField = tlmClass.getDeclaredField("table");
            // 設置ThreadLocal.ThreadLocalMap類內table字段的可見級別
            tableField.setAccessible(true);
            // 獲取當前線程的ThreadLocal.ThreadLocalMap類的 table字段的屬性值 (Entry[] table)
            Object[] entryArr = (Object[]) tableField.get(threadLocalMap);

            //遍歷ThreadLocal.ThreadLocalMap.table屬性 (Entry[] table)
            for (Object entry : entryArr) {
                if (entry != null) {
                    //獲取當前entry的class
                    Class<?> entryClass = entry.getClass();

                    //獲取當前entry的value字段並設置字段可見性
                    Field valueField = entryClass.getDeclaredField("value");
                    valueField.setAccessible(true);

                    //獲取當前entry的key字段並設置字段可見性
                    Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
                    referenceField.setAccessible(true);

                    //打印key/value
                    System.out.println(String.format("ThreadName=[%s],弱引用key=[%s],值=[%s]", currentThread.getName(),referenceField.get(entry), valueField.get(entry)));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

執行結果:
在這裏插入圖片描述
test1線程Debug詳情
在這裏插入圖片描述

test2線程Debug詳情
在這裏插入圖片描述

結論:ThreadLocal是弱引用,在發生GC時會自動會回收掉,但如果ThreadLocal對應的value是強引用則不會被回收,也就是會出現我們 value 沒被回收,key 被回收,導致 value 永遠存在,出現內存泄漏問題。

1.3.ThreadLocal重要屬性

// 當前 ThreadLocal 的 hashCode,由 nextHashCode() 計算而來,用於計算當前 ThreadLocal 在 ThreadLocalMap 中的索引位置
private final int threadLocalHashCode = nextHashCode();

// 哈希魔數,主要與斐波那契散列法以及黃金分割有關
private static final int HASH_INCREMENT = 0x61c88647;

// 保證了在一臺機器中每個 ThreadLocal 的 threadLocalHashCode 是唯一的
private static AtomicInteger nextHashCode = new AtomicInteger();
/*
* threadLocalHashCode`是ThreadLocal的散列值,定義爲final,表示ThreadLocal一旦創建其散列值就已經確定了
* 生成過程則是調用`nextHashCode():`
*/

// 返回計算出的下一個哈希值,其值爲 i * HASH_INCREMENT,其中 i 代表調用次數
private static int nextHashCode() {
	//該函數簡單地通過一個增量HASH_INCREMENT來生成hashcode。
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//nextHashCode表示分配下一個ThreadLocal實例的threadLocalHashCode的值,
//HASH_INCREMENT則表示分配兩個ThradLocal實例的threadLocalHashCod的增量。

/*
至於爲什麼這個增量爲0x61c88647?
	主要是因爲ThreadLocalMap的初始大小爲16,每次擴容都會爲原來的2倍,這樣它的容量永遠爲2的n次方,
	該增量選爲0x61c88647也是爲了儘可能均勻地分佈,減少碰撞衝突。
*/

其中的 HASH_INCREMENT也不是隨便取值的

  • 轉換爲十進制是 1640531527,2654435769
  • 轉換成 int 類型就是-1640531527,2654435769
  • 等於(√5-1)/2 乘以 2 的 32 次方(√5-1)/2就是黃金分割數,近似爲0.618,也就是說 0x61c88647可以理解爲一個黃金分割數乘以 2 的 32 次方,它可以保證nextHashCode 生成的哈希值,均勻的分佈在2 的冪次方上,且小於 2 的 32 次方

示例代碼:

private static final int HASH_INCREMENT = 0x61c88647;

public static void main(String[] args) throws Exception {
    int n = 5;
    int max = 2 << (n - 1);
    for (int i = 0; i < max; i++) {
        System.out.print(i * HASH_INCREMENT & (max - 1));
        System.out.print(" ");

    }
}

運行結果爲:0 7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25
.
可以發現元素索引值完美的散列在數組當中,並沒有出現衝突。

2.ThreadLocal.ThreadLocalMap

  • 因爲一個線程內可以存在多個 ThreadLocal 對象,所以其實是 ThreadLocal 內部維護了一個 Map ,這個 Map 不是直接使用的 HashMap ,而是 ThreadLocal 實現的一個叫做 ThreadLocalMap 的靜態內部類。
  • ThreadLocalMap使用用Entry類來進行存儲,key爲當前ThreadLocal對象的引用,value爲我們要存儲的值,
    -我們使用的 ThreadLocal.get()、ThreadLocal.set(),ThreadLocal.remove()方法其實都是 底層先獲取這個這個ThreadLocalMap,然後調用這個map對應的 get()、set() ,remove()來實現增刪改查。

源碼如下:

static class ThreadLocalMap {
	/**
	 * 鍵值對實體的存儲結構
	 * Entry繼承WeakReference,所以Entry對應key的引用(ThreadLocal實例)是一個弱引用。)
	 */
	static class Entry extends WeakReference<ThreadLocal<?>> {
		// 當前線程關聯的 value,這個 value 並沒有用弱引用追蹤
		Object value;

		/**
		 * 構造鍵值對
		 *
		 * @param k k 爲 key,作爲 key 的 ThreadLocal 會被包裝爲一個弱引用
		 * @param v v 爲 value
		 */
		Entry(ThreadLocal<?> k, Object v) {
			super(k);
			value = v;
		}
	}

	// 初始容量,必須爲 2 的冪
	private static final int INITIAL_CAPACITY = 16;

	// 存儲 ThreadLocal 的鍵值對實體數組,長度必須爲 2 的冪
	private Entry[] table;

	// ThreadLocalMap 元素數量
	private int size = 0;

	// 擴容的閾值,默認是數組大小的三分之二(1.5倍)
	private int threshold;

   //----------------省略其他代碼-------------
}

源碼中發現 ThreadLocalMap 就是一個簡單的 Map 結構,底層是數組,有初始化大小,也有擴容閾值大小數組的元素是 EntryEntry 的 key 就是 ThreadLocal 的引用value 是 ThreadLocal 的值。ThreadLocalMap 解決 hash 衝突的方式採用的是 線性探測法如果發生衝突會繼續尋找下一個空的位置。

2.1.ThreadLocalMap.set()解析

	/**
	 * key 當前threadLocal的引用
	 * value 要存儲的值
	 */
private void set(ThreadLocal<?> key, Object value) {
    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    //獲取Entry數組長度
    int len = tab.length;
    // 根據 ThreadLocal 的散列值,查找對應元素在數組中的位置(計算 key 在數組中的下標)
    int i = key.threadLocalHashCode & (len-1);
    // 採用“線性探測法”,尋找合適位置(索引處爲空即是合適的位置)(如果發生衝突會繼續尋找下一個空的位置)
    for (ThreadLocal.ThreadLocalMap.Entry e = tab[i] ; e != null; e = tab[i = nextIndex(i, len)]) {
        //獲取該哈希值處的ThreadLocal對象
        ThreadLocal<?> k = e.get();
        // key 存在,直接覆蓋
        if (k == key) {
            e.value = value;
            return;
        }
        // key == null,但是存在值(因爲此處的e != null),說明之前的ThreadLocal對象已經被回收了
        if (k == null) {
            // 用新元素替換陳舊的元素
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // ThreadLocal對應的key實例不存在也沒有陳舊元素,new 一個
    tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
    int sz = ++size;
    // cleanSomeSlots 清楚陳舊的Entry(key == null)
    // 如果沒有清理陳舊的 Entry 並且數組中的元素大於了閾值(數組大小的三分之二),則進行擴容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
    // 擴容的過程也是對所有的 key 重新哈希的過程
        rehash();
}


/**
* 索引位置 + 1
*/
private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
}
  • ThreadLocalMap.set()方法和Map.put()方法差不多,但是有一點區別是:Map.put方法處理哈希衝突使用的是 鏈地址法,而set方法使用的 開放地址法

  • ThreadLocalMap.set()中的replaceStaleEntry()cleanSomeSlots(),這兩個方法可以清除掉key ==null的實例,防止內存泄漏

2.2.ThreadLocalMap.getEntry()解析

/**
* 當前ThreadLocal的引用
*/
private Entry getEntry(ThreadLocal<?> key) {
	//// 根據 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);
}

由於採用了開放定址法,所以當前key的散列值和元素在數組中的索引並不是完全對應的,首先取一個探測數(key的散列值),如果所對應的key就是我們所要找的元素,則返回,否則調用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)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

這裏有一個重要的地方,當key==null時,調用了expungeStaleEntry()方法,該方法用於處理key == null,有利於GC回收,能夠有效地避免內存泄漏。

3.ThreadLocal 的 get()方法解析

public T get() {
	// 返回當前 ThreadLocal 所在的線程
	Thread t = Thread.currentThread();
	// 從線程中拿到 ThreadLocalMap
	ThreadLocalMap map = getMap(t);
	if (map != null) {
		// 從 ThreadLocalMap 中拿到 entry
		ThreadLocalMap.Entry e = map.getEntry(this);
		// 如果不爲空,讀取當前 ThreadLocal 中保存的值
		if (e != null) {
			@SuppressWarnings("unchecked")
			T result = (T) e.value;
			return result;
		}
	}
	// 若 map 爲空,則對當前線程的 ThreadLocal 進行初始化,最後返回當前的 ThreadLocal 對象關聯的初值,即 value
	return setInitialValue();
}

//初始化當前線程的 ThreadLocal
private T setInitialValue() {
        T value = initialValue();//initialValue不重寫默認返回null
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
}

//getMap()方法可以獲取當前線程所對應的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

//初始化ThreadLocal方法
protected T initialValue() {
        return null;
}

//初始化當前線程的ThreadLocalMap
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
}

//ThreadLocalMap的構造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
			//初始化Entry數組容量
            table = new Entry[INITIAL_CAPACITY];
            //初始化第一個key的索引
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            //第一個Entry的存儲
            table[i] = new Entry(firstKey, firstValue);
            //長度爲1
            size = 1;
            //設置ThreadLocalMap的長度
            setThreshold(INITIAL_CAPACITY);
}

//設置ThreadLocalMap的長度
private void setThreshold(int len) {
            threshold = len * 2 / 3;
}

get 方法的主要流程爲:

  1. 先獲取到當前線程的引用
  2. 獲取當前線程內部ThreadLocalMap
  3. 如果 ThreadLocalMap 存在,通過ThreadLocalMap的getEntry()方法 獲取當前 ThreadLocal 對應的 value 值
  4. 如果 ThreadLocalMap 不存在或者找不到 value 值,則調用 setInitialValue() 進行初始化

get 方法的時序圖如下所示:
在這裏插入圖片描述
其中每個 Thread 的ThreadLocalMapthreadLocal 作爲key,保存自己線程的 value 副本,是保存在每個線程中,並沒有保存在 ThreadLocal 對象中。

其中 ThreadLocalMap.getEntry() 方法的源碼如下:

/**
 * 返回 key 關聯的鍵值對實體
 *
 * @param key threadLocal
 * @return
 */
private Entry getEntry(ThreadLocal<?> key) {
	int i = key.threadLocalHashCode & (table.length - 1);
	Entry e = table[i];
	// 若 e 不爲空,並且 e 的 ThreadLocal 的內存地址和 key 相同,直接返回
	if (e != null && e.get() == key) {
		return e;
	} else {
		// 從 i 開始向後遍歷找到鍵值對實體
		return getEntryAfterMiss(key, i, e);
	}
}

ThreadLocalMap 的 resize 方法
當 ThreadLocalMap 中的 ThreadLocal 的個數超過容量閾值時,ThreadLocalMap 就要開始擴容了,我們一起來看下 resize 的源代碼:

/**
 * 擴容,重新計算索引,標記垃圾值,方便 GC 回收
 */
private void resize() {
	Entry[] oldTab = table;
	int oldLen = oldTab.length;
	int newLen = oldLen * 2;
	// 新建一個數組,按照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();
			// 若有垃圾值,則標記清理該元素的引用,以便GC回收
			if (k == null) {
				e.value = null;
			} else {
				// 計算 ThreadLocal 在新數組中的位置
				int h = k.threadLocalHashCode & (newLen - 1);
				// 如果發生衝突,使用線性探測往後尋找合適的位置
				while (newTab[h] != null) {
					h = nextIndex(h, newLen);
				}
				newTab[h] = e;
				count++;
			}
		}
	}
	// 設置新的擴容閾值,爲數組長度的三分之二
	setThreshold(newLen);
	size = count;
	table = newTab;
}

resize 方法主要是進行擴容,同時會將垃圾值標記方便 GC 回收,擴容後數組大小是原來數組的兩倍。

4. ThreadLocal 的initialValue()解析

protected T initialValue() {
    return null;
}

在上面的代碼分析get()的過程中,我們發現如果沒有先set的話,即在ThreadLocalMap中查找不到對應的存儲,則會通過調用setInitialValue方法返回i,而在setInitialValue方法中,有一個語句是T value = initialValue(), 而默認情況下,initialValue方法返回的是null

該方法定義爲protected級別且返回爲null,所以我們在使用ThreadLocal的時候一般都應該重寫該方法。

注意:如果想在get之前不需要調用set就能正常訪問的話,必須重寫initialValue()方法。

5.ThreadLocal 的 set()解析

public void set(T value) {
	// 返回當前ThreadLocal所在的線程
	Thread t = Thread.currentThread();
	// 返回當前線程持有的ThreadLocalMap 
	ThreadLocalMap map = getMap(t);
	if (map != null) {
		// 如果 ThreadLocalMap 不爲空,則直接存儲<ThreadLocal, T>鍵值對
		map.set(this, value);
	} else {
		// 否則,需要爲當前線程初始化 ThreadLocalMap,並存儲鍵值對 <this, firstValue>
		createMap(t, value);
	}
}

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

set 方法的作用是把我們想要存儲的 value 給保存進去。set 方法的流程主要是:

  1. 先獲取到當前線程的引用
  2. 利用這個引用來獲取到 ThreadLocalMap
  3. 如果 ThreadLocalMap 爲空,則去創建一個 ThreadLocalMap
  4. 如果 ThreadLocalMap 不爲空,就利用 ThreadLocalMap 的 set 方法將 value 添加到 map 中

通過createMap可以看出最終的變量是放在了當前線程的 ThreadLocalMap中,並不是存在 ThreadLocal 上,ThreadLocal 可以理解爲只是ThreadLocalMap的封裝傳遞了變量值


set 方法的時序圖如下所示:
在這裏插入圖片描述
其中map就是我們上面講到的ThreadLocalMap,可以看到它是通過當前線程對象獲取到的 ThreadLocalMap,接下來我們看 getMap方法的源代碼:

/**
 * 返回當前線程 thread 持有的 ThreadLocalMap
 *
 * @param t 當前線程
 * @return ThreadLocalMap
 */
ThreadLocalMap getMap(Thread t) {
	return t.threadLocals;
}

getMap 方法的作用主要是獲取當前線程內的 ThreadLocalMap 對象可以看出,原來 threadLocals 是線程的一個屬性所以在多線程環境下 threadLocals 是線程安全的,下面讓我們看看 Thread 類中的相關代碼:
在這裏插入圖片描述
可以看出每個線程都有 ThreadLocalMap 對象,被命名爲 threadLocals,默認爲 null,所以每個線程的 ThreadLocals 都是隔離獨享的。

調用 ThreadLocalMap.set() 時,會把當前 threadLocal 對象作爲key,想要保存的對象作爲value,存入 map。
ThreadLocalMap.set() 的源碼如下

/**
 * 在 map 中存儲鍵值對<key, value>
 *
 * @param key   threadLocal
 * @param value 要設置的 value 值
 */
private void set(ThreadLocal<?> key, Object value) {
	Entry[] tab = table;
	int len = tab.length;
	// 計算 key 在數組中的下標
	int i = key.threadLocalHashCode & (len - 1);
	// 遍歷一段連續的元素,以查找匹配的 ThreadLocal 對象
	for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
		// 獲取該哈希值處的ThreadLocal對象
		ThreadLocal<?> k = e.get();

		// 鍵值ThreadLocal匹配,直接更改map中的value
		if (k == key) {
			e.value = value;
			return;
		}

		// 若 key 是 null,說明 ThreadLocal 被清理了,直接替換掉
		if (k == null) {
			replaceStaleEntry(key, value, i);
			return;
		}
	}

	// 直到遇見了空槽也沒找到匹配的ThreadLocal對象,那麼在此空槽處安排ThreadLocal對象和緩存的value
	tab[i] = new Entry(key, value);
	int sz = ++size;
	// 如果沒有元素被清理,那麼就要檢查當前元素數量是否超過了容量闕值(數組大小的三分之二),以便決定是否擴容
	if (!cleanSomeSlots(i, sz) && sz >= threshold) {
		// 擴容的過程也是對所有的 key 重新哈希的過程
		rehash();
	}
}

Thread、ThreadLocal 以及 ThreadLocalMap 的關係
在這裏插入圖片描述
從上面又可以看出,ThreadLocalMap是在ThreadLocal中使用內部類來編寫的,但對象的引用是在Thread中!

於是我們可以總結出:Thread爲每個線程維護了ThreadLocalMap這麼一個Map(類型是ThreadLocal.ThreadLocalMap,也就是說每個線程有一個自己的ThreadLocalMap ),而ThreadLocalMap保存的EntrykeyThreadLocal對象本身value則是要存儲的對象

一個ThreadLocal只能存儲一個Object對象,如果需要存儲多個Object對象那麼就需要多個ThreadLocal!!!
在這裏插入圖片描述

5.remove()

public void remove() {
	// 返回當前線程持有的 ThreadLocalMap 
	ThreadLocalMap m = getMap(Thread.currentThread());
	if (m != null) {
		// 從 ThreadLocalMap 中清理當前 ThreadLocal 對象關聯的鍵值對
		m.remove(this);
	}
}

remove 方法的時序圖如下所示:
在這裏插入圖片描述

  1. 根據當前線程的引用獲取到對應的ThreadLocalMap
  2. 如果ThreadLocalMap不爲空,調用它的remove方法,從 ThreadLocalMap 中清理當前 ThreadLocal 對象關聯的鍵值

六.ThreadLocal 內存泄漏

  1. ThreadLocal在ThreadLocalMap中是以一個弱引用身份被Entry中的Key引用的,因此如果ThreadLocal沒有外部強引用來引用它,那麼ThreadLocal會在下次GC時被回收。這個時候就會出現Entry中Key已經被回收,出現一個null Key的情況,外部讀取ThreadLocalMap中的元素是無法通過null Key來找到Value的。

    1. 弱引用即WeakReference,表示如果弱引用的指向的對象只存在弱引用這一條線路,則下次YGC時會被回收。
    2. 當僅僅只有ThreadLocalMap中的Entry的key指向ThreadLocal的時候,ThreadLocal會進行回收的!!!
  2. 因此如果當前線程的生命週期很長,一直存在,那麼其內部的ThreadLocalMap對象也一直生存下來,這些null key就存在一條強引用鏈的關係一直存在:Thread --> ThreadLocalMap-->Entry-->Value,這條強引用鏈會導致Entry不會回收,Value也不會回收,但Entry中的Key卻已經被回收的情況,造成內存泄漏。

  3. JVM團隊已經考慮到這樣的情況,並採取一些措施來保證ThreadLocal儘量不會內存泄漏:在ThreadLocal的get()、set()、remove()方法調用的時候會清除掉線程ThreadLocalMap中所有Entry中Key爲null的Value,並將整個Entry設置爲null,利於下次內存回收。

    如果說會出現內存泄漏,那只有在出現了 key 爲 null 的記錄後,沒有手動調用 remove() 方法,並且之後也不再調用 get()、set()、remove() 方法的情況下。

七.ThreadLocal 應用場景

ThreadLocal 的特性也導致了應用場景比較廣泛,主要的應用場景如下:

  • 方便同一個線程使用某一對象,避免不必要的參數傳遞;
  • 線程間數據隔離(每個線程在自己線程裏使用自己的局部變量,各線程間的ThreadLocal對象互不影響);
  • 獲取數據庫連接、Session、關聯ID(比如日誌的uniqueID,方便串起多個日誌);
  • Spring 事務管理器採用了 ThreadLocal
  • Spring MVC 的 RequestContextHolder 的實現使用了 ThreadLocal
  1. 每個線程需要有自己單獨的實例
  2. 實例需要在多個類/方法中共享,但不希望被多線程共享
    • 對於第一點,每個線程擁有自己實例,實現它的方式很多。例如可以在線程內部構建一個單獨的實例。ThreadLoca 可以以非常方便的形式滿足該需求。
    • 對於第二點,可以在滿足第一點(每個線程有自己的實例)的條件下,通過方法間引用傳遞的形式實現。ThreadLocal 使得代碼耦合度更低,且實現更優雅。擴展:

1)存儲用戶Session

private static final ThreadLocal threadSession = new ThreadLocal();

    public static Session getSession() throws InfrastructureException {
        Session s = (Session) threadSession.get();
        try {
            if (s == null) {
                s = getSessionFactory().openSession();
                threadSession.set(s);
            }
        } catch (HibernateException ex) {
            throw new InfrastructureException(ex);
        }
        return s;
    }

2)解決線程安全的問題
比如Java7中的SimpleDateFormat不是線程安全的,可以用ThreadLocal來解決這個問題:

public class DateUtil {
    private static ThreadLocal<SimpleDateFormat> format1 = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static String formatDate(Date date) {
        return format1.get().format(date);
    }
}

這裏的DateUtil.formatDate()就是線程安全的了。

  • Java8裏的 java.time.format.DateTimeFormatter是線程安全的
  • Joda time裏的DateTimeFormat也是線程安全的

這類場景阿里規範裏面也提到了
在這裏插入圖片描述

八.可繼承的ThreadLocal-InheritableThreadLocal

ThreadLocal只能用於存儲當前線程的變量。子類線程獲取不到父類線程的數據。inheritableThreadLocals就是用來解決父子線程獨立變量共享問題。

如果我在主線程中set一個值,這個時候我在新創建的線程中是讀取不到的,因爲Threadlocal不支持繼承性。

static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
/**
*測試在主線程中創建子線程,然後獲取ThreadLocal的值
*/
 @Test
public  void testMainCreateChildThread1() {
    threadLocal.set(1000);
    new Thread(() -> {
         System.out.println(Thread.currentThread()+"------"+threadLocal.get());
     }).start();
}

輸出結果:
Thread[Thread-0,5,main]------null

也就是說Threadlocal不支持繼承性主線程設置了值,在子線程中是獲取不到的。那我現在想要獲取主線程裏面的值要怎麼做?

Threadlocal有一個子類InheritableThreadLocal 專門用來解決父子線程獨立變量共享問題。

static ThreadLocal<Integer> integerInheritableThreadLocal = new InheritableThreadLocal<>();
/**
*測試在主線程中創建子線程,然後獲取InheritableThreadLocal的值
*/
@Test
public  void testMainCreateChildThread2() {
     integerInheritableThreadLocal.set(2000);
     new Thread(() -> {
        System.out.println(Thread.currentThread().getName()+"------"+integerInheritableThreadLocal.get());
     }).start();
}

輸出結果:
Thread[Thread-0,5,main]====2000

運行結果發現子線程是可以獲取到主線程設置的值的,那它是如何實現的?

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
        return parentValue;
    }

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

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

InheritableThreadLocal繼承Threadlocal的,並且把threadlocals給替換成inheritableThreadLocal麼替換成inheritableThreadLocals`後子線程就可以獲取到主線程設置的屬性了嗎?我們在看一下Thread類中init的的實現。

private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;
		//獲取主線程
        Thread parent = currentThread();
       
        //-------省略無關代碼--------------
        //-------省略無關代碼--------------
        
		/*
		*inheritThreadLocals 設置爲true並且父類線程inheritableThreadLocals有共享數據則
		*創建一個父類線程inheritableThreadLocals副本,然後複製給當前線程的inheritableThreadLocals變量來實現父子線程共享
		*/
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        //創建一個父類線程inheritableThreadLocals副本並設置到當前線程的inheritableThreadLocals中
        this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

Threadinit()方法可以看出,先獲取了當前線程(主線程)判斷當前線程父線程的inheritableThreadLocals不爲空的話就調用ThreadLocal.createInheritedMap方法賦值給子線程中的inheritableThreadLocals。
在這裏插入圖片描述
InheritableThreadLocal仍然有缺陷,一般我們做異步化處理都是使用的線程池,而InheritableThreadLocal是在new Thread中的init()方法給賦值的,而線程池是線程複用的邏輯,所以這裏會存在問題。

當然,有問題出現就會有解決問題的方案,阿里巴巴開源了一個TransmittableThreadLocal組件就可以解決這個問題,這裏就不再延伸,感興趣的可自行查閱資料。

九.爲什麼建議使用static修飾ThreadLocal?

  1. 首先static修飾的變量是在類加載時就分配好內存空間,在類卸載纔會被回收,這一點請明確.
    2.ThreadLocal是 ThreadLocalMap 中Entry 的 key,而用 static 修飾 ThreadLocal,保證了 ThreadLocal 有強引用在,也就是 Entry 的 key有被強引用指向,會一直存在,垃圾回收的時候不會被回收
  2. ThreadLocal的原理是在Thread內部有一個ThreadLocalMap的集合對象,他的key是ThreadLocal,value就是你要存儲的變量副本, 不同的線程的ThreadLocalMap是相互隔離的,如果變量ThreadLocal是非static的就會造成每次生成實例都要生成不同的ThreadLocal對象,雖然這樣程序不會有什麼異常,但是會浪費內存資源.造成內存泄漏.

十.ThreadLocal的注意事項

1.ThrealLocal髒數據和內存泄漏問題

入坑:

  1. 髒數據問題:線程複用導致產生髒數據。由於線程池會複用Thread對象,進而Thread對象中的threalLocals也會被複用,導致Thread對象在執行其他任務時通過get()方法獲取到之前任務設置的數據,從而產生髒數據。
  2. 內存泄漏問題:ThreadLocal通常是使用static關鍵字修飾的。如果開發人員單純寄希望於ThreadLocal對象失去引用後,觸發弱引用機制來回收Entry的Value,那麼就會導致內存泄漏,Entry的Value無法被回收。

脫坑:

  1. 解決髒數據:線程執行前重新調用set()設置值。線程複用導致產生髒數據,如果複用線程在執行下個任務之前調用set()重新設置值,那麼髒數據問題就不會出現了。
  2. 解決內存泄漏:線程執行完後調用remove()完成收尾工作。無法依託弱引用機制來回收Entry的Value,那就調用ThreadLocal的remove方法顯式清除。

最後,Entry的弱引用機制不是導致ThreadLocal內存泄漏的原因,它的存在只是增加了開發人員的理解難度,就算沒有弱引用機制,線程執行完不調用remove()清除也會存在內存泄漏問題。

2.ThreadLocal結合線程池的問題

當 ThreadLocal 配合線程池使用的時候,我們需要及時對 ThreadLocal 進行清理,清除與本線程綁定的 value 值,否則會出現意料之外的結果。

來看看沒有調用remove方法和有調用remove下的結果差異。

private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(2);
    for (int i = 0; i < 5; i++) {
        executorService.execute(()->{
            Integer before = threadLocal.get();
            threadLocal.set(before + 1);
            Integer after = threadLocal.get();
            System.out.println("before: " + before + ",after: " + after);
        });
    }
    executorService.shutdown();
}

沒有調用 remove 方法進行清理

before: 0,after: 1
before: 0,after: 1
before: 1,after: 2
before: 2,after: 3
before: 3,after: 4

可以看到出現了 before 不爲0的情況,這是因爲線程在執行完任務被複用了,被複用的線程使用了上一個線程操作的value對象,從而導致不符合預期。

加上調用remove方法的邏輯:

try {
    Integer before = threadLocal.get();
    threadLocal.set(before + 1);
    Integer after = threadLocal.get();
    System.out.println("before: " + before + ",after: " + after);
} finally {
    threadLocal.remove();
}
before: 0,after: 1
before: 0,after: 1
before: 0,after: 1
before: 0,after: 1
before: 0,after: 1

3. 線程池異步調用,requestId傳遞

因爲 org.slf4j.MDC 是基於ThreadLocal去實現的,異步過程中,子線程並沒有辦法獲取到父線程ThreadLocal存儲的數據,所以這裏可以自定義線程池執行器,修改其中的run()方法:

public class MyThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {

    @Override
    public void execute(Runnable runnable) {
        Map<String, String> context = MDC.getCopyOfContextMap();
        super.execute(() -> run(runnable, context));
    }

    @Override
    private void run(Runnable runnable, Map<String, String> context) {
        if (context != null) {
            MDC.setContextMap(context);
        }
        try {
            runnable.run();
        } finally {
            MDC.remove();
        }
    }
}

十一.ThreadLocal原理總結

  1. ThreadLocal是用來提供線程局部變量的,在線程內可以隨時隨地的存取數據,而且線程之間是互不干擾的。

  2. ThreadLocal實際上是在每個線程內部維護了一個ThreadLocalMap,這個ThreadLocalMap是每個線程獨有的,裏面存儲的是Entry對象,Entry對象實際上是個ThreadLocal的實例的弱引用,同時還保存了value值,也就是說Entry存儲的是鍵值對key就是ThreadLocal實例引用,value則是要存儲的數據

  3. TreadLocal的核心是底層維護的ThreadLocalMap,它的底層是一個自定義的哈希表增長因子是2/3,增長因子也可以叫做是一個閾值,底層定義爲threshold,當哈希表容量大於或等於閾值的3/4的時候就開始擴容底層的哈希表數組table

  4. ThreaLocalMap中存儲的核心元素是Entry,Entry是一個弱引用,所以在GC的時候,ThreadLocal如果沒有外部的強引用,它會被回收掉,這樣就會產生key爲null的Entry了,這樣也就產生了內存泄漏

  5. ThreadLocalget(), set()remove()的時候都會清除ThreadLocalMap中key爲null的Entry,如果我們不手動清除,就會造成內存泄漏,最佳做法是使用ThreadLocal就像使用鎖一樣,加鎖之後要解鎖,也就是用完就使用remove進行清理。

xxxxxx

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