Java併發基礎七:深入理解ThreadLocal

ThreadLocal是什麼?

ThreadLocal是線程本地變量,可以爲多線程的併發問題提供一種解決方式,當使用ThreadLocal維護變量時,ThreadLocal爲每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。

ThreadLocal使用場景

①多個線程去獲取一個共享變量時,要求獲取的是這個變量的初始值的副本。②每個線程存儲這個變量的副本,對這個變量副本的改變不去影響變量本身。③適用於多個線程依賴不同變量值完成操作的場景。

ThreadLocal類常用接口

void set(T value):設置當前線程的線程局部變量的值

T get():獲取當前線程所對應的線程局部變量

void remove():刪除當前線程局部變量的值,目的是爲了減少內存的佔用

T initialValue():該線程局部變量的初始值(默認值爲null),該方法是一個protected的懶加載方法,線程第1次調用get()或set(T value)時才執行在,而且也是爲了讓子類覆蓋而設計的。

使用案例:

Demo①:ThreadLocal 是一個泛型類,保證可以接受任何類型的對象。

public class ThreadLocalDemo {
   private static ThreadLocal<Index> index = new ThreadLocal(){
       @Override
       protected Object initialValue() {
           return new Index();
      }
  };
   private static class Index{
       private int num;

       public void incr(){
           num++;
      }
  }
   public static void main(String[] args) {
       for(int i=0; i<5; i++){
           new Thread(() ->{
               Index local = index.get();
               local.incr();
               System.out.println(Thread.currentThread().getName() + " " + index.get().num);
          }, "thread_" + i).start();
      }
  }
}

輸出結果:

thread_1 1

thread_0 1

thread_3 1

thread_4 1

thread_2 1

Demo②:SimpleDateFormat是非線程安全(共享變量calendar訪問沒有做到線程安全),ThreadLocal 可以確保每個線程都可以得到單獨的一個 SimpleDateFormat 的對象,那麼自然也就不存在競爭問題了。

ThreadLocal工作原理

ThreadLocal內部維護的是一個類似Map的ThreadLocalMap數據結構,而每個Thread類,都有一個ThreadLocalMap成員變量。ThreadLocalMap將線程本地變量(ThreadLocal)作爲key,線程變量的副本作爲value,如圖所示:

實現原理:ThreadLocal底層實現是ThreadLocalMap數據結構,當使用ThreadLocal維護變量時,ThreadLocalMap將線程本地變量(ThreadLocal)作爲key,線程變量的副本作爲value。每個線程去使用共享變量時,實際調用threadLocal的get()方法,獲取當前線程對應的ThreadLocalMap,然後在根據key獲取value值,就實現了線程安全的操作變量副本的值了。

ThreadLocal源碼解析

想要熟悉和理解 Threadlocal 的源碼的話,我建議先思考這麼三個問題:

1、 Threadlocal 爲什麼能實現每個線程能有一個獨立的變量副本;

2、每個線程的變量副本的儲存位置在哪兒;

3、變量副本是如何從共享變量中複製出來的;

首先我們來看 initialValue( ) 方法:返回的是本地線程變量的初始值。返回值爲空的原因很簡單,這個方法就是用來重寫

* @return the initial value for this thread-local
     */
    protected T initialValue() {
        return null;
    }

②get()源碼分析

2.1 get()源碼入口

public T get() {
    //獲取當前線程
         Thread t = Thread.currentThread();
    //獲取當前線程的ThreadLocalMap
         ThreadLocalMap map = getMap(t);
         if (map != null) {
             //如果ThreadLocalMap已經被創建了,那麼通過當前的threadLocal對象作爲key,獲取value
             ThreadLocalMap.Entry e = map.getEntry(this);
             if (e != null) {
                 @SuppressWarnings("unchecked")
                 T result = (T)e.value;
                 return result;
            }
        }
    //如果ThreadLocalMap還沒有被創建或者在ThreadLocalMap中查找不到此元素
         return setInitialValue();
    }

 2.1.1 ThreadLocalMap沒初始化,ThreadLocalMap爲null時,會調用setInitialValue()方法:


private T setInitialValue() {
    //initialValue方法一般會被重寫,返回變量,不重寫的話,直接返回null
         T value = initialValue();
         Thread t = Thread.currentThread();
    //獲取當前線程的ThreadLocalMap
         ThreadLocalMap map = getMap(t);
         if (map != null)
             //ThreadLocalMap已經被創建,那麼直接設置初始值(即保存變量副本),初始值來自initialValue方法
             map.set(this, value);
         else
             //創建ThreadLocalMap
             createMap(t, value);
         return value;
    }

其中,initialValue()方法是由我們重寫的,需要注意的是,返回值必須爲new一個對象,而不是直接返回一個對象引用。因爲如果多個線程都保存同一個引用的副本的話,那他們通過這個引用修改共享變量的值,是相互影響的。我們本來的目的便是爲了獲取共享變量的初始值副本,各個線程對副本的修改不影響變量本身。這就是能實現每個線程能有一個獨立的變量副本原因

2.1.2 看看createMap是如何創建threadLocalMap的:

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

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
      //創建一個初始容量爲16的Entry數組
             table = new Entry[INITIAL_CAPACITY];
    //通過threadLocal的threadLocalHashCode來定位在數組中的位置
             int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    //保存在數組中
             table[i] = new Entry(firstKey, firstValue);
    //記錄下已用的大小
             size = 1;
    //設置閾值爲容量的2/3
             setThreshold(INITIAL_CAPACITY);
        } 

 2.2 初始化threadLocalMap之後,此線程再次調用get()方法,又做了哪些操作呢

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
                 //如果定位的元素的key與傳入的key不相等,那麼一直往後找
                 return getEntryAfterMiss(key, i, e);
        }

 可以看到是通過map.getEntry(this)去查找元素的,返回Entry。

2.3 如果map.getEntry(this)也找不到元素怎麼辦?回顧前面講的get入口,先判斷是否能根據當前線程獲取threadLocalMap,第一種情況:threadLocalMap爲空,那麼直接新初始化創建一個。第二請情況:threadLocalMap有值,但是map.getEntry(this) 爲空,這個時候就會在初始化方法裏調用map.set(this, value)方法,將當前參數設置進Map。

private T setInitialValue() {
    //initialValue方法一般會被重寫,不重寫的話,直接返回null
         T value = initialValue();
         Thread t = Thread.currentThread();
    //獲取當前線程的ThreadLocalMap
         ThreadLocalMap map = getMap(t);
         if (map != null)
             //ThreadLocalMap已經被創建,那麼直接設置初始值(即保存變量副本),初始值來自initialValue方法
             map.set(this, value);
         else
             //創建ThreadLocalMap
             createMap(t, value);
         return value;
    }
private void set(ThreadLocal<?> key, Object value) {
             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;
                }
            }

    //如果在table中確實找不到,那麼新建一個
             tab[i] = new Entry(key, value);
             int sz = ++size;
             if (!cleanSomeSlots(i, sz) && sz >= threshold)
                 //如果沒有元素被清除,且超過閾值,那麼擴容並重新hash定位
                 rehash();
        }

③set()源碼分析

ThreadLocalMap的set方法和get方法很類似


public void set(T value) {
   //獲取當前線程
   Thread t = Thread.currentThread();
   //獲取當前線程的ThreadLocalMap
   ThreadLocalMap map = getMap(t);
   if (map != null)
       map.set(this, value);
   else
       createMap(t, value);
}

底層源碼工作原理總結

首先使用ThreadLocal<?>維護變量時,重寫initialValue()方法,返回線程本地變量的初始值。然後每個線程去使用共享變量時,實際調用threadLocal的get()方法,獲取當前線程對應的ThreadLocalMap。進入 get函數,先判斷是否能根據當前線程獲取threadLocalMap,第一種情況:threadLocalMap爲空,那麼直接新初始化創建一個。第二請情況:threadLocalMap有值,但是map.getEntry(this) 爲空,這個時候就會在初始化方法裏調用map.set(this, value)方法,將當前參數設置進Map。第三種情況:threadLocalMap有值,map.getEntry(this) 有值 根據key獲取value直接返回,前兩種情況返回初始化value,實現安全訪問。

threadLocal的set()方法,作用:設置當前線程的線程局部變量的值,實現根據當前線程獲取對應的threadLocalMap,獲取到map直接將值set進去,獲取map爲空,直接創建threadLocalMap將值設置進去。

二、ThreadLocal的內存泄露分析

在分析ThreadLocal導致的內存泄露前,需要普及瞭解一下內存泄露、強引用與弱引用以及GC回收機制,這樣才能更好的分析爲什麼ThreadLocal會導致內存泄露呢?更重要的是知道該如何避免這樣情況發生,增強系統的健壯性。

內存泄露

內存泄露是程序在申請內存後,無法釋放已申請的內存空間,一次內存泄露危害可以忽略,但內存泄露堆積後果很嚴重,無論多少內存,遲早會被佔光

通俗的講:內存一直被對象或者變量佔用,導致內存不能被回收

強引用,使用最普遍的引用,一個對象具有強引用,不會被垃圾回收器回收。當內存空間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不回收這種對象。

如果想取消強引用和某個對象之間的關聯,可以顯式地將引用賦值爲null,這樣可以使JVM在合適的時間就會回收該對象。

弱引用,JVM進行垃圾回收時,無論內存是否充足,都會回收被弱引用關聯的對象。在java中,用java.lang.ref.WeakReference類來表示。可以在緩存中使用弱引用。

GC回收機制-如何找到需要回收的對象

JVM如何找到需要回收的對象,方式有兩種:

  • 引用計數法:每個對象有一個引用計數屬性,新增一個引用時計數加1,引用釋放時計數減1,計數爲0時可以回收,

  • 可達性分析法:以根集對象爲起始點進行搜索,如果有對象不可達的話,即沒有引用指向的對象,就是垃圾對象,jvm垃圾回收的時候將會對垃圾對象進行回收。(根集一般包括Java棧中引用的對象,方法區常量池中引用的對象,堆中引用的對象等)

不可達定義:在java中,對象是通過引用使用的,如果在沒有引用指向該對象的情況下,那麼將無從處理或調用該對象,這樣的對象爲不可達。

ThreadLocal的內存泄露分析

由於Thread中包含變量ThreadLocalMap,因此ThreadLocalMap與Thread的生命週期是一樣長,線程一直沒有完成,如果都沒有手動刪除對應key,都會導致內存泄漏。源碼開發也考慮到了這一點

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

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

但這次發現:Entry是繼承的WeakReference,並且只綁定了ThreadLocal(WeakReference表示弱引用對象)。

使用弱引用可以多一層保障:弱引用ThreadLocal不會內存泄漏,但是value就不同了,它是強引用,對應的value在下一次ThreadLocalMap調用set(),get(),remove()的時候會被清除。

防止內存泄漏最直接的方法就是使用完變量後調用ThreadLocal的remove(),remove()實際是將對象的引用置爲null,這樣一來沒有引用指向這個對象,該對象就會被JVM判定爲垃圾並在GC時回收掉。

三、ThreadLocal在set()時發生哈希衝突怎麼辦嗎

數據是以鍵值對方式存進Entry數組的,在存入時會根據鍵(ThreadLocal)的哈希值,找到它所存放的位置,但這樣有時會出現哈希衝突,至於如何應對哈希衝突

  1. 如果該位置是空的,那麼直接將鍵值對存儲;

  2. 若不爲空且兩個鍵相同,那麼新值換舊值;

  3. 若不爲空且兩鍵不相同,那隻能找下個空位置了。

文章參考:

https://mp.weixin.qq.com/s/I3hLAcA_cbBG25MGfqKFuw

https://mp.weixin.qq.com/s/B2-sZ9rO8xyrag2j-Xaqlg

https://mp.weixin.qq.com/s/BFNIDxWGJJy_NkIdi9Z7uQ

 

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