深刻理解Java併發編程之把ThreadLocal扣爛

基本含義

ThreadLocal字面意思是線程局部變量,它爲每個線程提供了獨立的互不干擾的局部變量。html

  1. ThreadLocal類是一個泛型類,也就是說這個局部變量能夠是各類類型,好比:Long,List等等。
  2. ThreadLocal類提供了get和set方法以在線程運行週期內獲取和改變這個局部變量的值。
  3. 每個線程的線程局部變量ThreadLocal是相互獨立,互不干擾的
  4. 線程局部變量ThreadLocal能夠提供一個初始化方法,對於當前線程沒有值的ThreadLocal變量會在第一次get()時,調用初始化方法initialValue進行初始化。該方法是一個延遲調用方法。

下面以一個簡單的例子來簡單介紹下ThreadLocal的使用:java

public class ThreadLocalDemo2 {
    public static class MyRunnable implements Runnable {
        private ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
            protected Integer initialValue() {
                return 1;
            }
        };

        @Override
        public void run() {
            threadLocal.set((int) (Math.random() * 100D));
            System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
        }
    }

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();

        Thread t1 = new Thread(myRunnable, "A");
        Thread t2 = new Thread(myRunnable, "B");

        t1.start();
        t2.start();
    }
    /**
     B:48
     A:32
     即:線程A與線程B中ThreadLocal保存的整型變量是各自獨立的,互不相干,只要在每一個線程內部使用set方法賦值,
     而後在線程內部使用get就能取到對應的值。
     */
}
複製代碼

實現原理

ThreadLocal的set(T value), get()源碼

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    
    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();
    }
    
    public static native Thread currentThread();
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
複製代碼
  1. 不論是set仍是get方法,首先經過native currentThread()方法拿到當前運行線程,而後拿到當前線程t對象實例上的類型爲ThreadLocalMap的threadLocals字段。
  2. 對於set方法,獲取到線程上的ThreadLocalMap後,若是存在直接set值;若是不存在,根據set的值初始化一個Map。
  3. 對於get方法,獲取到線程上的ThreadLocalMap後,若是存在直接get獲取值並類型轉換;若是不存在,調用setInitialValue()方法設置和返回初值。

注意web

ThreadLocal所存儲的變量的實際值是經過ThreadLocalMap結構存在Thread類的成員變量上的,也就是說每個Java線程,Thread類的對象實例,都有一個本身的ThreadLocalMap數組

ThreadLocalMap

ThreadLocalMap是ThreadLocal.java中的一個靜態內部類,它是一個爲了維護線程局部變量(ThreadLocal)定製化的哈希表。瀏覽器

static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
		
		...
        private Entry[] table;
		...
		
	}
複製代碼
  1. 這個ThreadLocalMap的Key爲泛型類ThreadLocal的實例,Value爲要存儲的ThreadLocal變量T。tomcat

  2. 實際ThreadLocal變量T與ThreadLocal的實例一塊兒做爲一個Entry,存儲在table裏面。bash

  3. 注意到這裏的ThreadLocalMap.Entry是繼承WeakReference使得做爲Key ThreadLocal的實例爲一個弱引用。那麼,在ThreadLocal的實例僅存在弱引用的且被GC線程掃描到的時候,就會GC回收掉threadLocal實例的內存。這個時候,對應的Key值就爲null了。服務器

  4. 這裏設計爲Map是因爲一個線程可能有多個線程局部變量即多個ThreadLocal的對象實例。dom

ThreadLocalMap哈希衝突

上面說到ThreadLocalMap,是一個定製化的哈希表。既然是哈希表就須要解決哈希衝突的問題。對於java.util.HashMap,解決衝突的方式是拉鍊法ide

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); 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; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } 複製代碼

而ThreadLocalMap解決衝突的方式是開放定址法

以set操做爲例,簡單的說就是經過Key作一次Hash以後,若是發現哈希結果對應位置的key和當前要set的key不一致,就日後面找,直到找到一個空的位置。

那麼,可不可能找不到呢?

答案是不可能的。

  1. 上面源碼的nextIndex方法保證下標到數組結尾後就又從table數組開頭尋找。
  2. 若是找不到,則表示當前table數組已經滿了。然而,上面源碼保證,每次數組大小達到threshold就會觸發擴容resize。因此,擴容必定發生在table數組變滿以前。

開放定址法的反作用

很差的反作用 因爲使用了開放定址法,致使ThreadLocalMap的set,get,remove操做都不能在一次哈希尋址肯定找到正確的位置。須要再花費O(n)的時間進行二次尋址去找到空位置或者是能獲取、刪除的位置。

好的反作用 JDK源碼做者經過另外一種方式利用了開放定址法帶來的二次尋址的循環。在set和get方法的二次尋址的循環過程當中,若是發現了stale entry(即key值爲空,可是Entry值非空,這裏也能夠理解爲value值非空)的位置,就會進行清理。

  • ThreadLocalMap -> set -> replaceStaleEntry -> expungeStaleEntry

  • ThreadLocalMap -> get -> getEntry -> getEntryAfterMiss -> expungeStaleEntry

private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;
			...
        }
複製代碼

注意:這種方式並不能保證,每次ThreadLocalMap.set 或者 get操做都能清除掉全部的key被回收的entry節點。舉一個極端的反例,ThreadLocalMap有key爲null的entry,可是get操做的第一次hash就直接找到了正確的位置,並無進行二次尋找。那麼,此時就沒法進行清除。

爲何ThreadLocal要用WeakReference

上面提到,ThreadLocalMap::Entry::ThreadLocal是一個弱引用。那麼,爲何要用WeakReference呢?

這裏,咱們反向思考下,若是不使用弱引用,而使用強引用。那麼,在線程的整個生命週期內,全部定義的ThreadLocal變量都一直存在,即便是用戶已經再也不使用ThreadLocal變量了,這是由於下面2條的引用關係鏈一直存在:

  • ThreadLocalRef->Thread->ThreadLocal->ThreadLocalMap->Entry->key

  • ThreadLocalRef->Thread->ThreadLocal->ThreadLocalMap->Entry->value

那麼,若是用戶不進行手動的ThreadLocalMap::remove,所佔用的空間就一直釋放不掉。

綜上,我理解的使用ThreadLocalMap->Entry->key(即ThreadLocal)使用弱引用的緣由是爲了在用戶沒有進行手動的ThreadLocalMap::remove狀況下,也能讓系統有方法在set,get的時候進行部分的資源清理。雖然,JVM只清理了key,可是後續JDK源碼設計提供了清理value以及整個entry的機制(將value和entry在ThreadLocalMap中的強引用給消除掉)。可是,這機制不必定能用上

So,每次肯定ThreadLocal再也不使用後,都要手動調用它的remove()方法進行數據清除。

否則,就可能會出現內存泄露。

內存泄露

在ThreadLocal變量僅持有弱引用的時候,若是經歷了GC就會被清除掉內存。而後,ThreadLocalMap的ThreadLocal key就變成null了。可是,對應的value因爲上面所寫的強引用關係鏈還一直存在,就沒發被回收。因而,就發生了value值沒發被獲取和使用,可是又沒法被回收的狀況,即內存泄漏。

應用場景

舉幾個例子說明一下:

  1. 好比線程中處理一個很是複雜的業務,可能方法有不少,那麼,使用 ThreadLocal 能夠代替一些參數的顯式傳遞
  2. 好比用來存儲用戶 Session。Session 的特性很適合 ThreadLocal ,由於 Session 以前當前會話週期內有效,會話結束便銷燬。咱們先籠統但不正確的分析一次 web 請求的過程:
  • 用戶在瀏覽器中訪問 web 頁面;
  • 瀏覽器向服務器發起請求;
  • 服務器上的服務處理程序(例如tomcat)接收請求,並開啓一個線程處理請求,期間會使用到 Session ;
  • 最後服務器將請求結果返回給客戶端瀏覽器。

從這個簡單的訪問過程咱們看到正好這個 Session 是在處理一個用戶會話過程當中產生並使用的,若是單純的理解一個用戶的一次會話對應服務端一個獨立的處理線程,那用 ThreadLocal 在存儲 Session ,簡直是再合適不過了。可是例如 tomcat 這類的服務器軟件都是採用了線程池技術的,並非嚴格意義上的一個會話對應一個線程。並非說這種狀況就不適合 ThreadLocal 了,而是要在每次請求進來時先清理掉以前的 Session ,通常能夠用攔截器、過濾器來實現。

最後,以爲寫的不錯的同窗麻煩點個贊,支持一下唄^_^~

參考與感謝

  1. juejin.im/post/5a64a5…
  2. blog.csdn.net/eson_15/art…
  3. www.imooc.com/article/267…
  4. www.cnblogs.com/fengzheng/p…
  5. www.jqhtml.com/58671.html
相關文章
相關標籤/搜索