Java併發之ThreadLocal

    在Java中,對象是線程共享的,當我們想讓線程獨自擁有專屬於自己的變量時,可以使用ThreadLocal類。

    ThreadLocal類提供了線程局部(Thread-Locak)變量。這些變量不同於他們的普通對應物,因爲訪問某個變量的每個線程都有自己的局部變量。當使用ThreadLocal維護變量時,ThreadLocal爲每個使用該變量的線程提供獨立的變量副本。所以,每一個線程都可以獨立地改變自己的副本,而不會影響其他線程所對應的副本。

    從線程的角度看,目標變量就是線程的本地變量,這也是Local所代表的含義。 

一、使用方法

      ThreadLocal只有4個方法,如下:
  • public T get():返回此線程局部變量的當前線程副本中的值。如果變量沒有用於當前線程的值,則先將其初始化爲調用 initalValue() 方法返回的值。
  • protected T initialValue():返回此線程局部變量的當前線程的”初始值“。線程第一次使用 get() 方法會調用此方法。但如果線程之前調用了 set() 方法,則不會對該線程再調用 initialValue() 方法。通常,此方法對每個線程最多調用一次,但如果在調用 get() 後又使用 remove() 方法,則可能再次調用此方法。  
  • public void remove():移除此線程局部變量當前線程的值。如果此線程局部變量隨後被當前線程讀取,且這期間線程沒有設置其值,則將調用其 initialValue() 方法重新初始化其值。這將導致在當前線程多次調用 initialValue() 方法。
  • public void set(T value):將此線程局部變量的當前線程副本中的值設置爲指定值。大部分子類不需要重寫此方法,它們只依靠 initialValue() 方法來設置線程局部變量的值。

二、ThreadLocal實現原理

    其實在ThreadLocal類中有一個靜態內部類 ThreadLocalMap ,用鍵值對的形式儲存每一個線程的變量副本,ThreadLocalMap中元素的key爲當前ThreadLocal對象,而Value對應線程的變量副本。每個線程對象t都擁有自己的ThreadLocalMap字段。線程t的ThreadLocalMap中存有多個ThreadLocal-Value鍵值對。
    ThreadLocal.ThreadLocalMap threadLocals = null; //Thread對象中含有字段 ThreadLocalMap

    get方法

    get方法返回當前線程中,存有的ThreadLocal局部變量。如果當前線程中沒有存入該值,將調用initialValue方法返回的值。源碼如下:

    public T get() {
        Thread t = Thread.currentThread();			//當前線程
        ThreadLocalMap map = getMap(t);				//獲取該線程的ThreadLocalMap
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);	//獲取map中鍵爲該ThreadLocal的entry
            if (e != null) {					//若有值 則返回該值
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();				//若沒有該值 則調用initialValue方法
    }

    getMap(Thread t) 方法用來獲取線程中的ThreadLocalMap:

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;		//獲取線程t的局部變量表
    }

    setInitialValue()方法將獲取初始值,並往線程的局部變量表中填入threadLocal-Value 鍵值對。如果還沒有局部變量表,將爲該線程創建。

    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;
    }

    set方法

    set方法將往線程的局部變量表中填入變量副本的鍵值對。

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);		//獲取局部變量表
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);		
    }

   remove方法

    remove方法將清除線程局部變量表中的該threadLocal鍵值對。

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

以上幾個方法的核心都是對線程的ThreadLocalMap進行操作,下面我們將查看ThreadLocalMap的源碼。

三、ThreadLocalMap實現原理

static class ThreadLocalMap {
  //map中的每個節點Entry,其鍵key是ThreadLocal並且還是弱引用,這也導致了後續會產生內存泄漏問題的原因。
 static class Entry extends WeakReference<ThreadLocal<?>> {
           Object value;
           Entry(ThreadLocal<?> k, Object v) {
               super(k);
               value = v;
   }
    /**
     * 初始化容量爲16,以爲對其擴充也必須是2的指數 
     */
    private static final int INITIAL_CAPACITY = 16;
    /**
     * 真正用於存儲線程的每個ThreadLocal的數組,將ThreadLocal和其對應的值包裝爲一個Entry。
     */
    private Entry[] table;


    ///....其他的方法和操作都和map的類似
}

    總之,爲不同線程創建不同的ThreadLocalMap,用線程本身爲區分點,每個線程之間其實沒有任何的聯繫,說是說存放了變量的副本,其實可以理解爲爲每個線程單獨new了一個對象。

四、內存泄漏問題

    網上大家討論說ThreadLocal會導致內存泄漏,原因如下:

  • 首先ThreadLocal實例被線程的ThreadLocalMap實例持有,也可以看成被線程持有。
  • 如果應用使用了線程池,那麼之前的線程實例處理完之後出於複用的目的依然存活
  • 所以,ThreadLocal設定的值被持有,導致內存泄露。
    上面的邏輯是清晰的,可是ThreadLocal並不會產生內存泄露,因爲ThreadLocalMap做選擇key的時候,並不是直接選擇ThreadLocal實例,而是ThreadLocalMap實例的弱引用。



    

發佈了222 篇原創文章 · 獲贊 4 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章