關於ThreadLocal的那些事

項目中看到了個ThrealLocal,交互數據庫都用到了它~ 雖然被封裝起來了,但我還是看看它到底啥模樣?此類優秀文章很多,自己寫下總結方便日後溫習。(基於jdk1.8)

一、什麼是Threadlocal?

Threadlocal,顧名思義 本地線程啦。
官方說明:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

基本用法

ThreadLocal<T> local = new ThreadLocal<T>();
local.set();
local.get()

此類提供線程局部變量。這些變量與正常變量不同,因爲每個訪問一個線程(通過其{@code get}或{@code set}方法)的線程都有其自己的,獨立初始化的變量副本。 {@code ThreadLocal}實例通常是希望將狀態與線程相關聯的類中的私有靜態字段(例如用戶ID或交易ID)。

 

簡而言之就是:
TreadLocal可以給我們提供一個線程內的局部變量,而且這個變量與一般的變量還不同,它是每個線程獨有的,與其他線程互不干擾的通過get和set方法就可以得到當前線程對應的值。
實際上是ThreadLocal的靜態內部類ThreadLocalMap爲每個Thread都維護了一個數組table,ThreadLocal確定了一個數組下標,而這個下標就是value存儲的對應位置

思考: 1: 是否絕對安全 2:使用場景

二、Threadlocal的作用

ThreadLocal是解決線程安全問題一個很好的思路,它通過爲每個線程提供一個獨立的變量副本解決了變量併發訪問的衝突問題。
在很多情況ThreadLocal比直接使用synchronized同步機制解決線程安全問題更簡單,更方便,且結果程序擁有更高的併發性。

三、Threadlocal的使用場景

在Java的多線程編程中,爲保證多個線程對共享變量的安全訪問,通常會使用synchronized來保證同一時刻只有一個線程對共享變量進行操作。這種情況下可以將類變量放到ThreadLocal類型的對象中,使變量在每個線程中都有獨立拷貝,不會出現一個線程讀取變量時而被另一個線程修改的現象。最常見的ThreadLocal使用場景爲用來解決數據庫連接、Session管理等。

四、Threadlocal的使用場景

在Java的多線程編程中,爲保證多個線程對共享變量的安全訪問,通常會使用synchronized來保證同一時刻只有一個線程對共享變量進行操作。這種情況下可以將類變量放到ThreadLocal類型的對象中,使變量在每個線程中都有獨立拷貝,不會出現一個線程讀取變量時而被另一個線程修改的現象。最常見的ThreadLocal使用場景爲用來解決數據庫連接、Session管理等。

五、Threadlocal的源碼解析

ThreadLocal類中提供了幾個方法:

1.public T get() { }

2.public void set(T value) { }

3.public void remove() { }

4.protected T initialValue(){ 

作爲一個存儲數據的類,關鍵點就在get和set方法。當然線程結束時防止產生 辣雞 垃圾,需要及時remove掉存儲的變量值。

 

1 . 看看ThreadLocalMap的set方法

//set 方法
public void set(T value) {
      //獲取當前線程
      Thread t = Thread.currentThread();
      //實際存儲的數據結構類型
      ThreadLocalMap map = getMap(t);
      //如果存在map就直接set,沒有則創建map並set
      if (map != null)
          map.set(this, value);
      else
          createMap(t, value);
  }
  
//getMap方法
ThreadLocalMap getMap(Thread t) {
      //thred中維護了一個ThreadLocalMap
      return t.threadLocals;
 }
 
//createMap
void createMap(Thread t, T firstValue) {
      //實例化一個新的ThreadLocalMap,並賦值給線程的成員變量threadLocals
      t.threadLocals = new ThreadLocalMap(this, firstValue);
}

可以看出每個線程持有一個ThreadLocalMap對象。每一個新的線程Thread都會實例化一個ThreadLocalMap(用於存取數據)並賦值給成員變量threadLocals,使用時若已經存在threadLocals則直接使用已經存在的對象。

既然來到了ThreadLocalMap,來看看它是什麼妖魔鬼怪

在ThreadLocalMap中其實是維護了一張哈希表,這個表裏面就是Entry對象,而每一個Entry對象簡單來說就是存放了我們的key和value值。

/**
 * The table, resized as necessary.
 * table.length MUST always be a power of two.
 */
private Entry[] table;
/**
 * The initial capacity -- MUST be a power of two.
 */
//初始容量, 想想HashMap也是16
private static final int INITIAL_CAPACITY = 16; 

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        //位運算,結果與取模相同,計算出需要存放的位置
        //threadLocalHashCode比較有趣
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
}

//Entry爲ThreadLocalMap靜態內部類,對ThreadLocal的若引用
//同時讓ThreadLocal和儲值形成key-value的關係
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
           super(k);
            value = v;
    }
}

可以看出實例化ThreadLocalMap時創建了一個長度爲16的Entry數組.通過hash Code與length位運算確定出一個索引值i,這個i就是被存儲在table數組中的位置,通過操作table來讀取。
顯然table是set和get的焦點,在看具體的set和get方法前,先看下面這段代碼。

//在某一線程聲明瞭ABC三種類型的ThreadLocal
ThreadLocal<A> sThreadLocalA = new ThreadLocal<A>();
ThreadLocal<B> sThreadLocalB = new ThreadLocal<B>();
ThreadLocal<C> sThreadLocalC = new ThreadLocal<C>();

由前面我們知道對於一個Thread來說只有持有一個ThreadLocalMap,所以ABC對應同一個ThreadLocalMap對象。爲了管理ABC,於是將他們存儲在一個數組的不同位置,而這個數組就是上面提到的Entry型的數組table。 那麼問題來了,ABC在table中的位置是如何確定的?爲了能正常夠正常的訪問對應的值,肯定存在一種方法計算出確定的索引值i

//ThreadLocalMap中set方法。
private void set(ThreadLocal<?> key, Object value) {

	// We don't use a fast path as with get() because it is at
	// least as common to use set() to create new entries as
	// it is to replace existing ones, in which case, a fast
	// path would fail more often than not.

	Entry[] tab = table;
	int len = tab.length;
	//獲取索引值,這個地方是比較特別的地方 (擴容)
	int i = key.threadLocalHashCode & (len-1);

	//遍歷tab如果已經存在則更新值
	for (Entry e = tab[i];
			 e != null;
			 e = tab[i = nextIndex(i, len)]) {
			ThreadLocal<?> k = e.get();

			if (k == key) {
					e.value = value;
					return;
			}

			if (k == null) {
					replaceStaleEntry(key, value, i);
					return;
			}
	}
	
	//如果上面沒有遍歷成功則創建新值
	tab[i] = new Entry(key, value);
	int sz = ++size;
	//滿足條件數組擴容x2
	if (!cleanSomeSlots(i, sz) && sz >= threshold)
			rehash();
}

在ThreadLocalMap中的set方法與構造方法能看到以下代碼片段。

  • int i = key.threadLocalHashCode & (len-1)
  • int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)

簡而言之就是將threadLocalHashCode進行 (初始容量減1之後)一個位運算(取模)得到索引i,threadLocalHashCode代碼如下。

private final int threadLocalHashCode = nextHashCode();

/**
 * The next hash code to be given out. Updated atomically. Starts at
 * zero.
 */
private static AtomicInteger nextHashCode =
		new AtomicInteger();

/**
 * The difference between successively generated hash codes - turns
 * implicit sequential thread-local IDs into near-optimally spread
 * multiplicative hash values for power-of-two-sized tables.
 */
private static final int HASH_INCREMENT = 0x61c88647;

/**
 * Returns the next hash code.
 */
private static int nextHashCode() {
	//自增
	return nextHashCode.getAndAdd(HASH_INCREMENT);
}


因爲static,在每次new ThreadLocal時因爲threadLocalHashCode的初始化,會使threadLocalHashCode值自增一次,增量爲0x61c88647; 0x61c88647是 斐波那契散列乘數,它的優點是通過它散列(hash)出來的結果分佈會比較均勻,可以很大程度上避免hash衝突。
 

小結set:

  • 對於某一ThreadLocal來講,他的索引值i是確定的,在不同線程之間訪問時訪問的是不同的table數組的同一位置即都爲table[i],只不過這個不同線程之間的table是獨立的。
  • 對於同一線程的不同ThreadLocal來講,這些ThreadLocal實例共享一個table數組,然後每個ThreadLocal實例在table中的索引i是不同的。

 


想起一個數學家的笑話,

  • 中午食堂的湯咋樣?
  • 你說他們的斐波那契湯啊!

2 . 接下來看看ThreadLocalMap的get方法

//ThreadLocal中get方法
public T get() {
    Thread t = Thread.currentThread();
    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;
        }
    }
    return setInitialValue();
}

/**
 * Variant of set() to establish initialValue. Used instead
 * of set() in case user has overridden the set() method.
 *
 * @return the initial value
 */
// 直接調用get,調用該方法初始化ThreadLocalMap ,代碼一目瞭然,熟悉的createMap
private T setInitialValue() {
		T value = initialValue();
		Thread t = Thread.currentThread();
		ThreadLocalMap map = getMap(t);
		if (map != null)
				map.set(this, value);
		else
				createMap(t, value);
		return value;
}

    
//ThreadLocalMap中getEntry方法
private Entry getEntry(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);
   }

通過set方法, get方法也就清晰了,取數據,但是我們會發現這裏的get並沒像 java 其他數組一樣傳下標去取,hash而來的下表,無非是通過計算出索引直接從數組對應位置讀取即可。

六、ThreadLocal特性

ThreadLocal和Synchronized都是爲了解決多線程中相同變量的訪問衝突問題,不同的點是

  • 1. Synchronized是通過線程等待,犧牲時間來解決訪問衝突
  • ThreadLocal是通過每個線程單獨一份存儲空間,犧牲空間來解決衝突,並且相比於Synchronized,ThreadLocal具有線程隔離的效果,只有在線程內才能獲取到對應的值,線程外則不能訪問到想要的值。

七 、ThreadLocal的內存泄露

先看張圖

aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy9IN3FVTjZZNFlVbDM1V3FNVjJoYVpqdUp3cEJHRkJMa3JGbVB1ZkJrS2ppYjhnejQ5aWNpYk0yZGtXaWFPbDFMaWJvYTd3cDhJT2xoaWJkWU5WVUVJYWxKZkRhUS82NDA_d3hfZm10PXBuZw.jpg

如上圖,每個線程找到自己維護的ThreadLocalMap,可以操作該數據結構,而ThreadLocalMap中維護的就是一個Entry數組,每個Entry對象就是我們存放的數據,它是個key-value的形式,key就是ThreadLocal實例的弱引用,value就是我們要存放的數據,也就是一個ThreadLocal的實例會對用一個數據,形成一個鍵值對。

1. 如何造成內存泄漏

Entry對象持有的是鍵就是ThreadLocal實例的弱引用,而弱引用會被(GC)回收掉,據上圖,圖中虛線就代表弱引用,如果這個ThreadLocal實例被回收掉,這個弱引用的鏈接也就斷開了,如下圖:

22321321BuZw.jpg

這樣則Entry對象中的key就變成了null,所以這個Entry對象就沒有被引用,因爲key變成看null,就取不到這個value值了,再加上如果這個當前線程遲遲沒有結束,ThreadLocalMap的生命週期就跟線程一樣,這樣就會存在一個強引用鏈,所以這個時候,key爲null的這個Entry就造成了內存泄漏。
 

對於這種賴着不走的,要及時remove掉。

每次使用ThreadLocal就會隨線程產生一個ThreadLocalMap,裏面維護Entry對象,我們對Entry進行存取值,那麼如果我們每次使用完ThreadLocal之後就把對應的Entry給刪除掉

ThreadLocal中提供了一個remove方法:

private T referent;         /* Treated specially by GC */

/**
* Remove the entry for key.
*/
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;
	}
    }
}

public void clear() {
      this.referent = null;
}

可以看到remove方法,根據key刪除掉對應的Entry,其中clear方法的註釋

 

參考: https://blog.csdn.net/sinat_33921105/article/details/103295070

還有一部分參考的是簡書的,emmm找不到原創了~ 總而言之, 參考 > 實踐 > 總結 ,一套下來,學習一個知識點就很牢固了 

 

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