線程、多線程之ThreadLocal簡述

1. ThreadLocal簡介

1.1 ThreadLocal概述      

       通常情況下,我們創建的變量是可以被任何一個線程訪問並修改的。如果想實現每一個線程都有自己的專屬本地變量該如何解決呢? JDK中提供的ThreadLocal類正是爲了解決這樣的問題。 ThreadLocal類主要解決的就是讓每個線程綁定自己的值,可以將ThreadLocal類形象的比喻成存放數據的盒子,盒子中可以存儲每個線程的私有數據。如果你創建了一個ThreadLocal變量,那麼訪問這個變量的每個線程都會有這個變量的本地副本,這也是ThreadLocal變量名的由來。他們可以用 get() 和 set() 方法來獲取默認值,或將值更改爲當前線程所存的副本的值,從而避免了線程安全問題。

       ThreadLocal是一個線程內部的存儲類,可以在指定線程內存儲數據,數據存儲以後,只有指定線程可以得到存儲數據。官方解釋如下:

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code 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提供了線程內存儲變量的能力,這些變量不同之處在於每一個線程讀取的變量是對應的互相獨立的。通過get() 和 set()方法就可以得到當前線程對應的值。從表面上看ThreadLocal相當於維護了一個map,key就是當前的線程ThreadLocal,value就是需要存儲的對象。實際上是ThreadLocal的靜態內部類ThreadLocalMap爲每個Thread都維護了一個數組table,ThreadLocal確定了一個數組下標,而這個下標就是value存儲的對應位置。

1.2 ThreadLocal內部分析

       作爲一個存儲數據的類,關鍵點就在get和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);
}

       再通俗一點來說,就像有兩個人去藏寶處收集寶物,這兩個共用一個揹包的話肯定會產生爭執,但是如果給他們兩個人每個人分配一個揹包的話就不會出現這樣的問題。如果把這兩個人比作線程的話,那麼ThreadLocal就是用來避免這兩個線程競爭的揹包,各自執行互不影響。

2. ThreadLocal示例

2.1 示例       

      通過上面的描述,我們初步應該對ThreadLocal這個類有了一定的理解。來看個例子:

import java.text.SimpleDateFormat;
import java.util.Random;

public class ThreadLocalExample implements Runnable{

     // SimpleDateFormat 不是線程安全的,所以每個線程都要有自己獨立的副本
    private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalExample obj = new ThreadLocalExample();
        for(int i=0 ; i<10; i++){
            Thread t = new Thread(obj, ""+i);
            Thread.sleep(new Random().nextInt(1000));
            t.start();
        }
    }

    @Override
    public void run() {
        System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //formatter pattern is changed here by thread, but it won't reflect to other threads
        formatter.set(new SimpleDateFormat());

        System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
    }

}

Output:

Thread Name= 0 default Formatter = yyyyMMdd HHmm
Thread Name= 0 formatter = yy-M-d ah:mm
Thread Name= 1 default Formatter = yyyyMMdd HHmm
Thread Name= 2 default Formatter = yyyyMMdd HHmm
Thread Name= 1 formatter = yy-M-d ah:mm
Thread Name= 3 default Formatter = yyyyMMdd HHmm
Thread Name= 2 formatter = yy-M-d ah:mm
Thread Name= 4 default Formatter = yyyyMMdd HHmm
Thread Name= 3 formatter = yy-M-d ah:mm
Thread Name= 4 formatter = yy-M-d ah:mm
Thread Name= 5 default Formatter = yyyyMMdd HHmm
Thread Name= 5 formatter = yy-M-d ah:mm
Thread Name= 6 default Formatter = yyyyMMdd HHmm
Thread Name= 6 formatter = yy-M-d ah:mm
Thread Name= 7 default Formatter = yyyyMMdd HHmm
Thread Name= 7 formatter = yy-M-d ah:mm
Thread Name= 8 default Formatter = yyyyMMdd HHmm
Thread Name= 9 default Formatter = yyyyMMdd HHmm
Thread Name= 8 formatter = yy-M-d ah:mm
Thread Name= 9 formatter = yy-M-d ah:mm

       從控制檯輸出可以看出,Thread-0已經改變了formatter的值,但仍然是thread-2默認格式化程序與初始化值相同,其他線程也一樣。

3. ThreadLocal實現原理

3.1 源碼分析

       從 Thread類源代碼入手,具體如下:

public class Thread implements Runnable {

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. 
     * 與此線程有關的ThreadLocal值。由ThreadLocal類維護   
    */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     * 與此線程有關的InheritableThreadLocal值。由InheritableThreadLocal類維護   
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

       從上面Thread類 源代碼可以看出Thread 類中有一個 threadLocals 和 一個 inheritableThreadLocals 變量,它們都是 ThreadLocalMap 類型的變量,我們可以把 ThreadLocalMap 理解爲ThreadLocal 類實現的定製化的 HashMap。默認情況下這兩個 變量都是null,只有當前線程調用 ThreadLocal 類的 setget方法時才創建它們,實際上調用這兩個方法的時候,我們調用的是ThreadLocalMap類對應的 get()set()方法。

3.2 ThreadLocal類的set和get方法

ThreadLocal類的set()方法

       實例化ThreadLocalMap時創建了一個長度爲16的Entry數組。通過hashCode與length位運算確定出一個索引值i,這個i就是被存儲在table數組中的位置。每個線程Thread持有一個ThreadLocalMap類型的實例threadLocals,結合此處的構造方法可以理解成每個線程Thread都持有一個Entry型的數組table,而一切的讀取過程都是通過操作這個數組table完成的。

//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構造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        //內部成員數組,INITIAL_CAPACITY值爲16的常量
        table = new Entry[INITIAL_CAPACITY];
        //位運算,結果與取模相同,計算出需要存放的位置
        //threadLocalHashCode比較有趣
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
}
//在某一線程聲明瞭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();
 }

    索引的計算

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

 簡而言之就是將threadLocalHashCode進行一個位運算(取模)得到索引i:

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

ThreadLocal類的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();
}
    
//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);
}

       通過上面這些內容,我們足以通過猜測得出結論:最終的變量是放在了當前線程的 ThreadLocalMap 中,並不是存在 ThreadLocal 上,ThreadLocal 可以理解爲只是ThreadLocalMap的封裝,傳遞了變量值。 ThrealLocal 類中可以通過Thread.currentThread()獲取到當前線程對象後,直接通過getMap(Thread t)可以訪問到該線程的ThreadLocalMap對象。每個Thread中都具備一個ThreadLocalMap,而ThreadLocalMap可以存儲以ThreadLocal爲key ,Object 對象爲 value的鍵值對。比如我們在同一個線程中聲明瞭兩個 ThreadLocal 對象的話,會使用 Thread內部都是使用僅有那個ThreadLocalMap 存放數據的,ThreadLocalMap的 key 就是 ThreadLocal對象,value 就是 ThreadLocal 對象調用set方法設置的值。

3.3 ThreadLocal圖解

ThreadLocal數據結構

ThreadLocalMapThreadLocal的靜態內部類。

ThreadLocal內部類

4. ThreadLocal內存泄漏問題分析

   ThreadLocalMap 中使用的 key 爲 ThreadLocal 的弱引用,而 value 是強引用。所以,如果 ThreadLocal 沒有被外部強引用的情況下,在垃圾回收的時候,key 會被清理掉,而 value 不會被清理掉。這樣一來,ThreadLocalMap 中就會出現key爲null的Entry。假如我們不做任何措施的話,value 永遠無法被GC 回收,這個時候隨着value的佔用越來越多,而且內存得不到有效的釋放,就可能會產生內存泄露。ThreadLocalMap實現中已經考慮了這種情況,在調用 set()get()remove() 方法的時候,會清理掉 key 爲 null 的記錄。使用完 ThreadLocal方法後 最好手動調用remove()方法

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

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }關於對象引用的幾種類型請參考博主的:https://blog.csdn.net/qq_39470733/article/details/88182953

5. ThreadLocal特性

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

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

       正因爲ThreadLocal的線程隔離特性,使他的應用場景相對來說更爲特殊一些。在android中Looper、ActivityThread以及AMS中都用到了ThreadLocal。當某些數據是以線程爲作用域並且不同線程具有不同的數據副本的時候,就可以考慮採用ThreadLocal。

如有披露或問題歡迎留言或者入羣探討

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