(七)全面剖析Java并發編程之線程變量副本ThreadLocal原理分析

引言

在之前的文章:徹底理解Java并發編程之Synchronized關鍵字實現原理剖析中我們曾初次談到線程安全問題引發的"三要素":多線程、共享資源/臨界資源、非原子性操作,簡而言之:在同一時刻,多條線程同時對臨界資源進行非原子性操作則有可能產生線程安全問題。而如果想要解決線程安全問題,我們只需要破壞掉三要素中的任意條件即可,如下:

  • ①破壞多線程條件:同一時刻,一條線程對共享資源進行非原子性操作,不會產生線程安全問題
  • ②破壞共享資源條件:同一時刻多條線程對局部資源進行非原子性操作,也不會產生線程安全問題
  • ③破壞非原子性條件:同一時刻多條線程對共享資源進行原子性操作,也不會產生線程安全問題

“三要素”說法僅是個人理解,如有疑義可糾正

而在前面的文章中,我們曾談到過CAS無鎖機制、Synchronized隱式鎖、ReetrantLock顯式鎖等都可以解決線程安全問題。而在這些方案當中,CAS機制是利用上面第三點:破壞非原子性條件,保證原子性來解決線程安全問題;Synchronized與ReetrantLock則是利用上述第一點:破壞多線程條件,在同一時刻只允許一條線程訪問臨界資源解決此問題。而本文談到的ThreadLocal則是通過如上第二點:破壞共享資源條件解決線程安全問題。

一、ThreadLocal概念及使用淺析

ThreadLocal線程本地副本,在很多地方也被稱為線程本地變量、線程局部存儲等叫法,但總歸來說都是形容ThreadLocal這一個東西。在執行時,ThreadLocal會為變量在每一條線程創建一個副本,這個副本只有每條線程自己可以訪問。下面我們可以先看看ThreadLocal類以及它提供的一些方法:

// 省略方法體(后面源碼再詳細分析)
public class ThreadLocal<T> {
    // 構造函數
    public ThreadLocal() {}
    
    // 初始化方法:在創建ThreadLocal對象時可以使用該方法進行初始化設值
    protected T initialValue()
    
    // 獲取ThreadLocal在當前線程中保存的變量副本
    public T get() 
    
    // 設置當前線程中變量的副本
    public void set(T value)
    
    // 移除當前線程中變量的副本
    public void remove()
    
    // 內部子類:擴展了ThreadLocal的初始化值的方法,支持Lambda表達式賦值
    static final class SuppliedThreadLocal<T> extends ThreadLocal<T>
    
    // 內部類:定制的hashMap,僅用于維護當前線程的本地變量值。
    // 僅ThreadLocal類對其有操作權限,是Thread的私有屬性。
    // 為避免占用空間較大或生命周期較長的數據常駐于內存引發一系列問題,
    // hashtable的key是弱引用WeakReferences。
    // 當堆空間不足時,會清理未被引用的entry。
    static class ThreadLocalMap
    
    // 省略其他代碼.......
}

如上便是ThreadLocal提供的一些主要方法,在創建ThreadLocal對象時可以initialValue()對變量副本進行初始化,也可以使用set()方法更改值或者設置線程變量副本,使用get()方法獲取變量副本,而remove()則可以移除當前線程中變量的副本。我們先來看一個例子:

public class DBUtils {
    private static Connection connection = null;

    public static Connection getConnection() throws SQLException {
        if (connection == null)
            connection = DriverManager.getConnection(
                    "jdbc:mysql:127.0.0.1:3306/test?user=root&password=root");
        return connection;
    }

    public static void closeConnection() throws SQLException {
        if (connection != null)
            connection.close();
    }
}

假設有上面這么一個數據庫連接工具類DBUtils,如上代碼在單線程的環境下運行是沒有問題的,但是如果把這個工具類丟在多線程的情況下則會出現問題。很顯然,在獲取連接getConnection()方法中,同一時刻如果有多條線程同時執行if (connection == null)判斷則很有可能會導致創建多個連接對象。而因為connection是共享資源,所以在操作時也應該保證線程安全問題,不然在多線程情況下可能會造成:一條線程還在執行SQL,另外一條線程則調用closeConnection()方法關閉了連接對象。

所以如上這個例子我們該怎么解決遇到的問題?簡單~

public class DBUtils {
    private static volatile Connection connection = null;

    public synchronized static Connection getConnection() throws SQLException {
        if (connection == null)
            connection = DriverManager.getConnection(
                    "jdbc:mysql:127.0.0.1:3306/test?user=root&password=root");
        return connection;
    }

    public synchronized static void closeConnection() throws SQLException {
        if (connection != null)
            connection.close();
    }
}

我們在共享變量connection加上volatile關鍵字修飾以及在操作臨界資源的方法上添加synchronized關鍵字修飾,這樣就能保證線程安全。或者我們也可以這樣:

public class DBUtils {
    private static volatile Connection connection = null;
    private static ReentrantLock lock = new ReentrantLock();

    public static Connection getConnection() throws SQLException {
        lock.lock(); //獲取鎖
        if (connection == null)
            connection = DriverManager.getConnection(
                    "jdbc:mysql:127.0.0.1:3306/test?user=root&password=root");
        lock.unlock(); // 釋放鎖
        return connection;
    }

    public static void closeConnection() throws SQLException {
        lock.lock(); //獲取鎖
        if (connection != null)
            connection.close();
        lock.unlock(); // 釋放鎖
    }
}

但是上面的兩種方式確實可以保證線程安全,但是帶來的弊端也很明顯:

當一條線程在執行SQL時,其他線程只能等待當前線程先處理完成之后才可以獲取連接,這樣會大大的影響程序的效率。

我們可以思考一下,此處到底是否需要將connection對象變成共享資源?結果顯而易見,其實是不需要的,因為每條線程可以持有一個connection對象進行DB操作,每條線程之間對connection對象的操作是不存在任何依賴關系的。那我們能不能這樣?

public class DBUtils {
    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(
                    "jdbc:mysql:127.0.0.1:3306/test?user=root&password=root");;
    }

    public static void closeConnection(Connection connection) throws SQLException {
        if (connection != null)
            connection.close();
    }
}

理論上是可行的,因為由于每次線程操作DB時創建的都是不同的連接對象,自然也就不存在線程安全問題。但是由于線程每次訪問DB都需要創建一個新的連接對象,用完之后再次關閉,在執行過程中會頻繁的獲取/關閉數據庫連接,這樣不但影響系統整體效率,還會導致給DB服務器造成巨大的壓力,嚴重的情況下甚至會直接導致系統崩潰。

那么在這種情況下時,我們就可以使用ThreadLocal來解決此類問題,如下:

public class DBUtils {
    private static ThreadLocal<Connection> connectionHolder =
    new ThreadLocal<Connection>(){
        @SneakyThrows
        public Connection initialValue(){
            return DriverManager.getConnection(
                    "jdbc:mysql:127.0.0.1:3306/test?user=root&password=root");
        }
    };

    public static Connection getConnection() throws SQLException {
        return connectionHolder.get();
    }
}

在如上例子中,我們可以使用ThreadLocal為每個線程創建一個Connection變量副本,從而達到我們最開始所說的:ThreadLocal通過破壞共享資源條件解決線程安全問題,每條執行的線程操作的都是自己本地的副本變量,自然也就不構成“三要素”。

ThreadLocal使用場景

  • ①上下文(context)傳遞。一個對象需要在多個方法中層次傳遞使用,比如用戶身份、任務信息、調用鏈ID、關聯ID(如日志的uniqueID,方便串起多個日志)等,如果此時使用責任鏈模式給每個方法添加一個context參數會比較麻煩,而此時就可以使用ThreadLocal設置參數,需要使用時get一下即可。
  • ②線程間的數據隔離。如spring事務管理機制實現則使用到ThreadLocal來保證單個線程中的數據庫操作使用的是同一個數據庫連接。同時,采用這種方式可以使業務層使用事務時不需要感知并管理Connection連接對象,通過傳播級別,能夠巧妙地管理多個事務配置之間的切換,掛起和恢復。
  • ③ThreadLocal一般情況下,我們在項目開發過程中很少使用,而它更多應用則是在框架源碼中應用,如Spring框架的事務隔離機制中的TransactionSynchronizationManager類,也包括Netty框架中的二次封裝類FastThreadLocal等。
  • ④上個ThreadLocal的應用案例:
// 日期工具類
private static ThreadLocal<DateFormat> threadLocal = 
        ThreadLocal.withInitial(()-> 
            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

public static Date parse(String dateStr) {
    Date date = null;
    try {
        date = threadLocal.get().parse(dateStr);
    } catch (ParseException e) {
        e.printStackTrace();
    }
    return date;
}

二、ThreadLocal原理分析

在前面我們對ThreadLocal進行了簡單的講解,而ThreadLocal作為一個存儲類型的類,重點就是讀寫get()set()。現在我們則可以深入源碼去一探ThreadLocal的神秘面紗。

2.1、ThreadLocal創建變量副本原理分析

先從ThreadLocal.set()方法開始:

// ThreadLocal類 → set()方法
public void set(T value) {
    // 獲取當前執行線程
    Thread t = Thread.currentThread();
    // 獲取當前線程的threadlocals成員變量
    ThreadLocalMap map = getMap(t);
    // 如果map不為空,則將value添加進map
    if (map != null)
        map.set(this, value);
    // 如果map為空則先為當前線程創建一個map再將value加入map
    else
        createMap(t, value);
}

ThreadLocal.set()方法中總歸來說分為三步:

  • 調用getMap()獲取當前線程的ThreadLocalMap
  • 如果map不為空則將傳入的value值添加進map
  • 如果map為空則先為當前線程創建一個map再將value加入map

首先來看看getMap(Thread)方法:

// ThreadLocal類 → getMap()方法
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

是不是有些意外?在getMap(Thread)方法中是調用當前線程對象的成員變量threadLocals并返回的:

Thread類:ThreadLocal.ThreadLocalMap threadLocals = null;

可以看到,Thread類的成員變量threadLocals實則就是ThreadLocalMap,而ThreadLocalMap則是一個給ThreadLocal定制版的HashMap,也是ThreadLocal的內部類,如下:

// ThreadLocal類
public class ThreadLocal<T> {
    // ThreadLocal內部類:ThreadLocalMap
    static class ThreadLocalMap {
        // ThreadLocalMap內部類:Entry
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
}

ThreadLocalMap類中還存在一個內部類Entry,繼承自WeakReference弱引用類型,結構如下:

ThreadLocal結構圖

再來看看createMap()方法:

// ThreadLocal類 → createMap()方法
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

至此,我們應該已經明白了ThreadLocal是如何為每個線程創建變量副本的:

在每條線程Thread內部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個threadLocals就是每條線程用來存儲變量副本的,key值為當前ThreadLocal對象,value為變量副本(即T類型的變量)。每個Thread線程對象最開始的threadLocals都為空,當線程調用ThreadLocal.set()或ThreadLocal.get()方法時(get方法待會而會分析到),都會調用createMap()方法對threadLocals進行初始化。然后在當前線程里面,如果要使用副本變量,就可以通過get方法在threadLocals里面查找。

原理如下:


ThreadLocal創建變量副本原理

2.2、ThreadLocal獲取變量副本原理分析

在上述過程中,我們已經分析了ThreadLocal創建變量副本原理,接下來我們再看看ThreadLocal.get()方法:

// ThreadLocal類 → get()方法
public T get() {
    // 獲取當前執行線程
    Thread t = Thread.currentThread();
    // 獲取當前線程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 如果map不為空,將當前ThreadLocal對象作為key獲取對應值
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 如果獲取的值不為空則返回獲取到的value
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 如果map為空則調用setInitialValue方法
    return setInitialValue();
}

ThreadLocal.get()方法中,總歸來說分為三步:

  • 獲取到當前執行線程,通過getMap(Thread)方法獲取ThreadLocalMap類型的map
  • 將當前ThreadLocal對象this作為key嘗試獲取map中的<key,value>鍵值對,獲取成功返回value
  • 如果第一步獲取的map為空則調用setInitialValue()方法返回value

調用get()方法之后首先會獲取當前線程的threadLocals成員變量(即ThreadLocalMap),如map不為空則以為this作為key獲取ThreadLocal中存儲的變量副本,如果為空則調用setInitialValue()方法:

// ThreadLocal類 → setInitialValue()方法
private T setInitialValue() {
    // 獲取ThreadLocal初始化值
    T value = initialValue();
    Thread t = Thread.currentThread();
    // 獲取當前線程的map
    ThreadLocalMap map = getMap(t);
    // 如果map不為空則將初始化值添加進map容器
    if (map != null)
        map.set(this, value);
    // 如果map為空則創建一個ThreadLocalMap容器
    else
        createMap(t, value);
    return value;
}

// ThreadLocal類 → initialValue()()方法
protected T initialValue() {
    return null;
}

setInitialValue()與前面分析的ThreadLocal.set(value)方法有些類似,在setInitialValue()方法中首先會調用initialValue()方法獲取初始化值,而initialValue()方法默認是返回空的,但是initialValue()方法可以在創建ThreadLocal對象時進行重寫,如下:

private static ThreadLocal<Object> threadlocal =
new ThreadLocal<Object>(){
    @SneakyThrows
    public Object initialValue(){
        return new Object();
    }
};

獲取到初始化的值之后,再次獲取當前線程的threadLocals,如果不為空則以this為key,初始值為value添加進map。如果當前線程的threadLocals為空,則先調用createMap(t, value);為當前線程創建一個ThreadLocalMap并將this和初始值以k-v形式加入map中,然后并將value返回,如果沒有創建ThreadLocal對象時嗎沒有初始化值則返回null,至此整個ThreadLocal.get()方法結束。如下:

ThreadLocal獲取變量副本原理

三、InheritableThreadLocal詳解

通過上述的分析,不難得知ThreadLocal設計的目的就是為每條線程都開辟一塊自己的局部變量存儲區域(并不是為了解決線程安全問題設計的,不過使用ThreadLocal可以避免一定的線程安全問題產生),所以如果你想要將ThreadLocal中的數據共享給子線程時,實現起來將額外的困難。而InheritableThreadLocal則應運而生,InheritableThreadLocal可以實現多個線程訪問ThreadLocal的值,ok~。上個例子:

private static InheritableThreadLocal<String> itl = 
new InheritableThreadLocal<String>();
public static void main(String[] args) throws InterruptedException {
    System.out.println(Thread.currentThread().getName()
    + "......線程執行......");
    itl.set("竹子....");
    System.out.println("父線程:main線程賦值:竹子....");
    new Thread(()->{
        System.out.println(Thread.currentThread().getName()
        + "......線程執行......");
        System.out.println("子線程:T1線程讀值:"+itl.get());
    },"T1").start();
    System.out.println("執行結束.....");
}

如上代碼所示,創建一個InheritableThreadLocal類型變量itl,在父線程main中進行賦值操作,然后開啟一條子線程T1進行讀值操作,執行結果如下:

/*
 執行結果:
    main......線程執行......
    父線程:main線程賦值:竹子....
    執行結束.....
    T1......線程執行......
    子線程:T1線程讀值:竹子....
*/

從結果中不難看出,子線程T1讀取的值竟然是main父線程設置的值,這是為什么呢?下面我們看看InheritableThreadLocal的源碼:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    // 在父線程向子線程復制InheritableThreadLocal變量時使用
    protected T childValue(T parentValue) {
        return parentValue;
    }
    // 返回線程的inheritableThreadLocals成員變量
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
    // 為線程的成員變量inheritableThreadLocals進行初始化
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

InheritableThreadLocal中重寫了父類ThreadLocalgetMap()以及createMap()方法,在我們前面分析ThreadLocal時,曾提到過線程類Thread中存在一個成員變量threadlocals,而實則Thread中除開threadlocals成員之外,還存在另外一個成員變量inheritableThreadLocals,如下:

ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

所以當操作InheritableThreadLocal變量時只影響線程的inheritableThreadLocals成員,而并不影響`threadlocals``成員。

3.1、InheritableThreadLocal父子線程傳值原理

搞清楚InheritableThreadLocal構成之后,我們接著來分析一下父子線程傳值究竟是如何實現的。我們一般在創建子線程時,都是直接選擇new Thread()創建:

Thread t1 = new Thread();

接著會調用Thread類的構造函數創建線程對象:

// Thread類 → 構造函數
public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}

// Thread類 → init()方法重載
private void init(ThreadGroup g, Runnable target, String name,
                long stackSize) {
    // 調用全參的init方法完成線程初始化
    init(g, target, name, stackSize, null, true);
}

// Thread類 → init()方法
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }
    this.name = name;
    // 獲取當前執行線程作為父線程
    Thread parent = currentThread();
    SecurityManager security = System.getSecurityManager();
    if (g == null) {
        // 確認創建出的線程是否為子線程
        // 如果SecurityManager不為空則獲取SecurityManager的線程分組
        if (security != null) {
            g = security.getThreadGroup();
        }

        // 如果SecurityManager中沒有為創建出的線程設置線程分組,
        // 則使用當前執行的線程parent的父線程組
        if (g == null) {
            g = parent.getThreadGroup();
        }
    }

    // 無論是否顯式傳入threadgroup,都要檢查訪問
    g.checkAccess();

    // 如果SecurityManager不為空則檢查權限是否
    // 為SUBCLASS_IMPLEMENTATION_PERMISSION
    if (security != null) {
        if (isCCLOverridden(getClass())) {
            security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
        }
    }
    g.addUnstarted();
    // 將當前執行線程設置為創建出的線程的父線程
    this.group = g;
    this.daemon = parent.isDaemon();
    this.priority = parent.getPriority();
    // 獲取線程上下文類加載器
    if (security == null || isCCLOverridden(parent.getClass()))
        this.contextClassLoader = parent.getContextClassLoader();
    else
        this.contextClassLoader = parent.contextClassLoader;
    // 為當前創建出的線程設置線程上下文類加載器
    this.inheritedAccessControlContext =
            acc != null ? acc : AccessController.getContext();
    this.target = target;
    setPriority(priority);
    // 重點!!!后面詳細分析
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    // 為創建出的線程分配默認線程棧大小
    this.stackSize = stackSize;
    // 設置線程ID
    tid = nextThreadID();
}

如上便是線程創建時的初始化過程,在init()方法中有這么一段代碼:

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

當采用默認方式創建子線程時,一條線程執行new指令創建Thread對象的方式被稱為默認方式,而這種方式會將當前執行創建邏輯的線程設置為創建出來的線程的父線程。如果父線程的inheritableThreadLocals成員變量不為空,那么則會執行this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);,將父線程inheritableThreadLocals傳遞至子線程。接著可以再看看ThreadLocal.createInheritedMap()方法:

// ThreadLocal類 -> createInheritedMap()方法
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}

//  ThreadLocalMap類 -> 私有構造函數
// 構建一個包含所有parentMap中Inheritable ThreadLocals的ThreadLocalMap
// 該函數只被createInheritedMap()調用.
private ThreadLocalMap(ThreadLocalMap parentMap) {
    // 獲取父線程的所有Entry
    Entry[] parentTable = parentMap.table;
    // 獲取父線程的Entry數量
    int len = parentTable.length;
    setThreshold(len);
    // ThreadLocalMap使用Entry[] table存儲ThreadLocal
    table = new Entry[len];

    // 挨個復制父線程中map的Entry
    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                // 為什么這里不是直接賦值而是使用childValue方法?
                // 因為childValue內部是直接將e.value返回的,
                // 這樣實現的目的可能是為了保證代碼最大程度上的拓展性
                // 因為可以重寫childValue()覆蓋
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

當調用ThreadLocal.createInheritedMap()方法后會將父線程中inheritableThreadLocals成員的所有Entry全部復制一遍給子線程的inheritableThreadLocals成員,至此,整個創建過程完成。從這個流程中我們可以得知:父子線程傳值的實現是通過創建線程時復制inheritableThreadLocals的所有Entry實現的。

四、ThreadLocalMap原理剖析

ThreadLocal的原理是涉及三個核心類:ThreadLocalThread以及ThreadLocalMap類。在Thread類中存在兩個成員變量:threadLocalsinheritableThreadLocals,這兩個成員變量的類型都為ThreadLocalMap,經過一系列分析后我們可以得知,這兩個成員變量是存儲線程變量副本的最終容器,而前面也曾提到過:ThreadLocalMapThreadLocal中定制版的HashMap,但是它并沒有實現Map接口,而是自己內部通過數組類型存儲Entry實現。而Entry只是簡單的繼承了WeakReference軟引用,并沒有沒有實現類似HashMapNode.next的后繼節點指向,所以ThreadLocalMap并不是鏈表形式的實現。哪沒有了鏈表結構之后,ThreadLocalMap是如何解決哈希沖突的呢?下面可以從源碼角度分析得知:

// ThreadLocalMap類 → Entry靜態內部類
static class Entry extends WeakReference<ThreadLocal<?>> {
    // value:存儲的變量副本
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
           super(k);
            value = v;
    }
}

// ThreadLocalMap類 → 構造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 成員變量table(數組結構),INITIAL_CAPACITY值為16的常量
    table = new Entry[INITIAL_CAPACITY];
    // 位運算,類似于取模算法,計算出需要存放的位置
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

從如上代碼不難得知,在調用createMap()方法創建ThreadLocalMap示例時,在ThreadLocalMap的構造方法中,會為成員變量table初始化一個長度為16的Entry數組,通過hashCodelength位運算確定出一個下標索引值i,這個i就是被存儲在table數組中的下標位置。那么現在可以來個簡單的例子理解一下:

ThreadLocal<Zero> tl0 = new ThreadLocal<Zero>();
ThreadLocal<One> tl1 = new ThreadLocal<One>();
ThreadLocal<Two> tl2 = new ThreadLocal<Two>();

new Thread(()->{
    tl0.set(new Zero());
    tl1.set(new One());
    tl2.set(new Two());
},"T1").start();

new Thread(()->{
    tl0.set(new Zero());
    tl1.set(new One());
    tl2.set(new Two());
},"T2").start();

在案例中,創建了三個ThreadLocal對象:tl0、tl1、tl2以及兩個線程對象:T1、T2,經過前面分析我們知道,在每個Thread對象中都維護著一個ThreadLocalMap類型的成員變量threadlocals存儲每條線程的副本變量。所以,T1、T2內部分別都維護著一個ThreadLocalMap,當T1、T2操作tl0、tl1、tl2時,Zero、One、Two都會以key-value的形式存儲在數組的不同位置,這個數組就是前面提到的ThreadLocalMap類中的成員Entry[] table。哪又是怎么確定tl0-Zero、tl1-One、tl2-Two這三組K-Vtable中的存儲位置呢?如下:

  //ThreadLocalMap類 → set()方法
  private void set(ThreadLocal<?> key, Object value) {
    // 獲取table及其長度
    Entry[] tab = table;
    int len = tab.length;
    // 使用key的哈希值和數組長度計算獲取索引值
    int i = key.threadLocalHashCode & (len-1);

    // 遍歷table如果已經存在則更新值,不存在則創建
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        // 如果key相同,則使用新value替換老value
        if (k == key) {
            e.value = value;
            return;
        }
        // 如果table[i]為空則創建新的Entry存儲
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    
    // table[i]不為null且key不相同的情況下,
    // 如果遍歷完數組也沒有找到為null的位置,
    // 則代表數組需要擴容,則將數組擴容兩倍
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 如果清理過期的數據之后,數組內的可用數據還占
    // 3/4的情況下,直接擴容兩倍
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

我們可以從源碼中不難發現,在set()方法開始后,會首先獲取table的長度和ThreadLocal對象的哈希值用于計算出一個下標索引值iint i = key.threadLocalHashCode & (len-1);

// ThreadLocal中threadLocalHashCode相關代碼
private final int threadLocalHashCode = nextHashCode();

private static AtomicInteger nextHashCode =
    new AtomicInteger();

// 0x61c88647為斐波那契散列乘數,哈希得到的結果會比較分散
private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
    // 原子計數器自增
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

因為ThreadLocal中哈希碼相關的成員都是靜態static關鍵字修飾的原因,每次創建ThreadLocal對象時,都會在對象初始化的時候調用一次自增方法為ThreadLocal對象生成一個哈希值:

private final int threadLocalHashCode = nextHashCode();

HASH_INCREMENT=0x61c88647是因為0x61c88647為斐波那契散列乘數,通過它散列(hash)出來的結果分布會比較均勻,可以很大程度上避免hash沖突。
經過如上分析我們能夠得到一個結論:每條線程的threadlocals都會在內部維護獨立table數組,而每個ThreadLocal對象在不同的線程table中位置都是相同的。對于同一條線程而言,不同的ThreadLocal變量副本都會被封裝成一個個的Entry對象存儲在自己內部的table中。

ok~,接著往下說,經過int i = key.threadLocalHashCode & (len-1);計算出索引下標值之后,會開始遍歷table,然后會開始判斷,如果table[i]位置不為空,但是原本的key值和現在新的key值是相同的情況下,則使用現在的新值替換掉之前的老值,刷新value值并返回:

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

如果table[i]位置為空,則創建一個的Entry對象封裝K-V值并將該對象放在table[i]位置:

if (k == null) {
    replaceStaleEntry(key, value, i);
    return;
}

如果table[i]位置不為空并且Key不相同時,哪就調用nextIndex(i,len)獲取下一個位置信息并判斷下一個位置是否為空,直到找到為空的位置為止:

e = tab[i = nextIndex(i, len)] // 在for循環的末尾循環體

table[i]位置不為空并且Key不相同的情況下,如果遍歷完整個table數組也沒有找到為空的下標位置時,代表數組已經存滿了需要擴容,則調用rehash()對數組擴容兩倍:

// 滿足條件table數組擴容兩倍
if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();

至此整個ThreadLocalMap存儲過程結束,如下:

ThreadLocalMap存儲原理

接下來再看看ThreadLocalMap的get原理:

// ThreadLocal類 -> ThreadLocalMap內部類 -> getEntry()方法
private Entry getEntry(ThreadLocal<?> key) {
    // 通過`ThreadLocal`對象的哈希值跟`table`數組長度
    // 進行計算獲取下標索引值`i`
    int i = key.threadLocalHashCode & (table.length - 1);
    // 獲取table[i]位置的元素,如果不為空并且key相同則返回
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    // 如果key不相同則遍歷整個table[i]之后的元素獲取對應key的值
    else
        return getEntryAfterMiss(key, i, e);
}

// ThreadLocal類 -> ThreadLocalMap內部類 -> getEntryAfterMiss()方法
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    // 遍歷整個table[i]之后的元素
    while (e != null) {
        ThreadLocal<?> k = e.get();
        // 如果key相同則返回對應的元素
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

與前面分析的set同理,在get時,也會根據ThreadLocal對象的哈希值跟table數組長度進行計算獲取下標索引值i,然后判斷該位置Entry對象的key值與get(key)的key是否相同,如果相同則直接獲取該位置的值并返回。如果不相同則遍歷整個數組中table[i]之后的所有元素,循環判斷下一個位置的key是否與傳入進來的key一致,如果一致則獲取返回。

五、ThreadLocal注意事項

5.1、ThreadLocal線程安全問題

ThreadLocal雖然能夠在一定程度上解決線程安全問題,但ThreadLocal設計的初衷是為每條線程開辟一塊自己的存儲空間。所以如果ThreadLocal.set()的對象如果是共享的,多線程情況下也會造成線程安全問題的出現。

5.2、ThreadLocal副本變量的產生

ThreadLocal的變量并不是每條線程拷貝克隆一個對象,而是每個線程新建一個。

5.3、ThreadLocal在線程池情況下可能會產生臟數據

因為線程池會復用線程,而線程上一個執行的任務對ThreadLocal進行set()操作后,在線程run()結束后沒有調用remove()移除變量副本,下個Runnable任務如果直接對ThreadLocal進行get()操作則可能讀到臟數據。

5.4、ThreadLocal可能會造成內存泄露

ThreadLocalMap中存儲變量副本時,Entry對象使用ThreadLocal的弱引用作為key,如果一個ThreadLocal對象沒有外部強引用來指向它,在堆內存不足時GC機制會回收掉這些弱引用類型的key,則會造成ThreadLocalMap<null,Object>的情況,同時線程也遲遲不結束(比如線程池中的常駐線程),那么這些key=null的value值則會一直存在一條強引用鏈:Thread.threadlocals(Reference)成員變量 -> ThreadLocalMap對象 -> Entry對象 -> Object value對象導致GC無法回收造成內存泄露,這個Object就是泄露的對象。至于為什么要將key設置成弱引用類型的原因:

因為key如果不設計成弱引用類型的情況下,會造成entry中value出現內存泄漏的場景

解決方案

關于5.3和5.4的兩個問題,我們可以在使用完ThreadLocal手動調用ThreadLocal.remove()方法清空ThreadLocal變量副本即可解決。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,156評論 6 529
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,866評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 174,880評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,398評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,202評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,743評論 1 320
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,822評論 3 438
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 41,962評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,476評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,444評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,579評論 1 365
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,129評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,840評論 3 344
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,231評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,487評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,177評論 3 388
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,568評論 2 370

推薦閱讀更多精彩內容