【JavaSE】多線程(5)_ThreadLocal及其常用方法set()、get()、remove()詳解

本篇總結ThreadLocal的相關知識,它在實際開發中使用頻率較高,要認真學 ~

1 ThreadLocal

  • ThreadLocal是一種類型,用於提供 線程局部變量,在多線程環境下可以 保證各個線程裏的變量獨立於其他線程的變量
  • 即:ThreadLocal可爲每個線程創建一個單獨的變量副本,相當於線程的private static類型變量。
  • 在多線程環境下,同步機制是爲了保證數據的一致性,而ThreadLocal是爲了保證數據的獨立性。

2 ThreadLocal實現

public void set(T value)
public T get()
public void remove()
protected T initialValue()
  • set(T value)方法

    • 首先獲取當前線程,然後再獲取到當前線程的ThreadLocalMap,若它不爲null,則將value保存其中,並用當前的ThreadLocal作爲key;否則創建一個ThreadLocalMap並給到當前線程,然後保存value。
    • ThreadLocalMap相當於一個HashMap,是真正保存值的地方。
  • get()方法

    • 在 get() 方法中也會獲取到當前線程的 ThreadLocalMap,如果 ThreadLocalMap 不爲 null,則獲取 key 爲當前 ThreadLocal 的值;
    • 否則調用 setInitialValue() 方法返回初始值,並保存到新創建的 ThreadLocalMap 中。
  • remove()方法

    • 刪除當前key

3 ThreadLocalMap

  • 在 set,get,initialValue 和 remove 方法中都會獲取到當前線程,然後通過當前線程獲取到 ThreadLocalMap,如果ThreadLocalMap 爲 null,則會創建一個 ThreadLocalMap,並給到當前線程。

  • 可見,每個線程都會持有一個ThreadLocalMap用來維護線程本地的值。

  • 每個線程的ThreadLocalMap 是屬於線程自己的, ThreadLocalMap 中維護的值也是屬於線程自己的。這就保證了ThreadLocal 類型的變量在每個線程中是獨立的,在多線程環境下不會相互影響。

  • 構造方法
    ThreadLocal 中當前線程的 ThreadLocalMap 爲 null 時會使用ThreadLocalMap 的構造方法新建一個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);
}
  • 存儲結構
    ThreadLocalMap 內部維護一個哈希表(數組)來存儲數據,並且定義了加載因子。
// 初始容量,必須是 2 的冪
private static final int INITIAL_CAPACITY = 16;
// 存儲數據的哈希表
//這個table是一個Entry類型的數組,Entry是ThreadLocalMap的一個內部類。
//Entry 用於保存一個鍵值對,其中key以“弱引用”的方式保存。
private Entry[] table;
// table 中已存儲的條目數
private int size = 0;
// 表示一個閾值,當 table 中存儲的對象達到該值時就會擴容
private int threshold;
// 設置 threshold 的值
private void setThreshold(int len){
	threshold = len *2/3;
}
  • 保存鍵值對

    調用set(ThreadLocal key,Object value)方法將數據保存到哈希表中。

private void set(ThreadLocal key, Object value) {
	Entry[] tab = table;
	int len = tab.length;
	// 計算要存儲的索引位置
	int i = key.threadLocalHashCode & (len-1);
	// 循環判斷要存放的索引位置是否已經存在 Entry,若存在,進入循環體
	for (Entry e = tab[i];
	e != null;
	e = tab[i = nextIndex(i, len)]) {
		ThreadLocal k = e.get();
		// 若索引位置的 Entry 的 key 和要保存的 key 相等,則更新該 Entry 的值
		if (k == key) {
			e.value = value;
			return;
		}
		// 若索引位置的 Entry 的 key 爲 null(key 已經被回收了),表示該位置的 Entry 已經無效,用要保存的鍵值替換該位置上的 Entry
		if (k == null) {
			replaceStaleEntry(key, value, i);
			return;
		}
	}
	// 要存放的索引位置沒有 Entry,將當前鍵值作爲一個 Entry 保存在該位置
	tab[i] = new Entry(key, value);
	// 增加 table 存儲的條目數
	int sz = ++size;
	// 清除一些無效的條目並判斷 table 中的條目數是否已經超出閾值
	if (!cleanSomeSlots(i, sz) && sz >= threshold)
		rehash(); // 調整 table 的容量,並重新擺放 table 中的 Entry
}
  • 首先使用 key(當前 ThreadLocal)的 threadLocalHashCode 來計算要存儲的索引位置 i。threadLocalHashCode 的值由ThreadLocal 類管理,每創建一個 ThreadLocal 對象都會自動生成一個相應的 threadLocalHashCode 值。

  • 在保存數據時,如果索引位置有 Entry,且該 Entry 的 key 爲 null,就會清除無效 Entry 來騰出空間,因爲:Entry的 key 是弱引用,key 一旦被回收(即 key 爲 null),就無法再訪問到 key 對應的 value。

  • 在調整 table 容量時,也會先清除無效對象,然後再根據需要擴容。

  • 獲取Entry對象

    • 使用getEntry(ThreadLocal key)方法。
    • 因爲可能存在哈希衝突,key對應的Entry的存儲位置可能不在由key計算出的索引位置上,所以要調用getEntryAfterMiss(ThreadLocal key,int i,Entry e)方法獲取。
  • 內存泄露

    • 在ThreadLocalMap的set、get、remove方法中都有清除無效Entry的操作,和Entry的key使用弱引用的方式,都是爲了降低內存泄漏的概率,但不能完全避免。
    • 開發中每次用完ThreadLocal 都要調用remove方法,清除所有key爲null的value,防止內存泄漏。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章