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

引言

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

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

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

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

一、ThreadLocal概念及使用淺析

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

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

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

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

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

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

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關(guān)鍵字修飾以及在操作臨界資源的方法上添加synchronized關(guān)鍵字修飾,這樣就能保證線程安全?;蛘呶覀円部梢赃@樣:

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(); // 釋放鎖
    }
}

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

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

我們可以思考一下,此處到底是否需要將connection對象變成共享資源?結(jié)果顯而易見,其實(shí)是不需要的,因?yàn)槊織l線程可以持有一個connection對象進(jìn)行DB操作,每條線程之間對connection對象的操作是不存在任何依賴關(guān)系的。那我們能不能這樣?

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

理論上是可行的,因?yàn)橛捎诿看尉€程操作DB時創(chuàng)建的都是不同的連接對象,自然也就不存在線程安全問題。但是由于線程每次訪問DB都需要創(chuàng)建一個新的連接對象,用完之后再次關(guān)閉,在執(zhí)行過程中會頻繁的獲取/關(guān)閉數(shù)據(jù)庫連接,這樣不但影響系統(tǒng)整體效率,還會導(dǎo)致給DB服務(wù)器造成巨大的壓力,嚴(yán)重的情況下甚至?xí)苯訉?dǎo)致系統(tǒng)崩潰。

那么在這種情況下時,我們就可以使用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為每個線程創(chuàng)建一個Connection變量副本,從而達(dá)到我們最開始所說的:ThreadLocal通過破壞共享資源條件解決線程安全問題,每條執(zhí)行的線程操作的都是自己本地的副本變量,自然也就不構(gòu)成“三要素”。

ThreadLocal使用場景

  • ①上下文(context)傳遞。一個對象需要在多個方法中層次傳遞使用,比如用戶身份、任務(wù)信息、調(diào)用鏈ID、關(guān)聯(lián)ID(如日志的uniqueID,方便串起多個日志)等,如果此時使用責(zé)任鏈模式給每個方法添加一個context參數(shù)會比較麻煩,而此時就可以使用ThreadLocal設(shè)置參數(shù),需要使用時get一下即可。
  • ②線程間的數(shù)據(jù)隔離。如spring事務(wù)管理機(jī)制實(shí)現(xiàn)則使用到ThreadLocal來保證單個線程中的數(shù)據(jù)庫操作使用的是同一個數(shù)據(jù)庫連接。同時,采用這種方式可以使業(yè)務(wù)層使用事務(wù)時不需要感知并管理Connection連接對象,通過傳播級別,能夠巧妙地管理多個事務(wù)配置之間的切換,掛起和恢復(fù)。
  • ③ThreadLocal一般情況下,我們在項(xiàng)目開發(fā)過程中很少使用,而它更多應(yīng)用則是在框架源碼中應(yīng)用,如Spring框架的事務(wù)隔離機(jī)制中的TransactionSynchronizationManager類,也包括Netty框架中的二次封裝類FastThreadLocal等。
  • ④上個ThreadLocal的應(yīng)用案例:
// 日期工具類
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進(jìn)行了簡單的講解,而ThreadLocal作為一個存儲類型的類,重點(diǎn)就是讀寫get()set()?,F(xiàn)在我們則可以深入源碼去一探ThreadLocal的神秘面紗。

2.1、ThreadLocal創(chuàng)建變量副本原理分析

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

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

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

  • 調(diào)用getMap()獲取當(dāng)前線程的ThreadLocalMap
  • 如果map不為空則將傳入的value值添加進(jìn)map
  • 如果map為空則先為當(dāng)前線程創(chuàng)建一個map再將value加入map

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

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

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

Thread類:ThreadLocal.ThreadLocalMap threadLocals = null;

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

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

ThreadLocalMap類中還存在一個內(nèi)部類Entry,繼承自WeakReference弱引用類型,結(jié)構(gòu)如下:

ThreadLocal結(jié)構(gòu)圖

再來看看createMap()方法:

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

至此,我們應(yīng)該已經(jīng)明白了ThreadLocal是如何為每個線程創(chuàng)建變量副本的:

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

原理如下:


ThreadLocal創(chuàng)建變量副本原理

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

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

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

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

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

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

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

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

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

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

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

ThreadLocal獲取變量副本原理

三、InheritableThreadLocal詳解

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

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

如上代碼所示,創(chuàng)建一個InheritableThreadLocal類型變量itl,在父線程main中進(jìn)行賦值操作,然后開啟一條子線程T1進(jìn)行讀值操作,執(zhí)行結(jié)果如下:

/*
 執(zhí)行結(jié)果:
    main......線程執(zhí)行......
    父線程:main線程賦值:竹子....
    執(zhí)行結(jié)束.....
    T1......線程執(zhí)行......
    子線程:T1線程讀值:竹子....
*/

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

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

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

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

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

3.1、InheritableThreadLocal父子線程傳值原理

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

Thread t1 = new Thread();

接著會調(diào)用Thread類的構(gòu)造函數(shù)創(chuàng)建線程對象:

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

// Thread類 → init()方法重載
private void init(ThreadGroup g, Runnable target, String name,
                long stackSize) {
    // 調(diào)用全參的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;
    // 獲取當(dāng)前執(zhí)行線程作為父線程
    Thread parent = currentThread();
    SecurityManager security = System.getSecurityManager();
    if (g == null) {
        // 確認(rèn)創(chuàng)建出的線程是否為子線程
        // 如果SecurityManager不為空則獲取SecurityManager的線程分組
        if (security != null) {
            g = security.getThreadGroup();
        }

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

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

    // 如果SecurityManager不為空則檢查權(quán)限是否
    // 為SUBCLASS_IMPLEMENTATION_PERMISSION
    if (security != null) {
        if (isCCLOverridden(getClass())) {
            security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
        }
    }
    g.addUnstarted();
    // 將當(dāng)前執(zhí)行線程設(shè)置為創(chuàng)建出的線程的父線程
    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;
    // 為當(dāng)前創(chuàng)建出的線程設(shè)置線程上下文類加載器
    this.inheritedAccessControlContext =
            acc != null ? acc : AccessController.getContext();
    this.target = target;
    setPriority(priority);
    // 重點(diǎn)?。。『竺嬖敿?xì)分析
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    // 為創(chuàng)建出的線程分配默認(rèn)線程棧大小
    this.stackSize = stackSize;
    // 設(shè)置線程ID
    tid = nextThreadID();
}

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

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

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

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

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

    // 挨個復(fù)制父線程中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方法?
                // 因?yàn)閏hildValue內(nèi)部是直接將e.value返回的,
                // 這樣實(shí)現(xiàn)的目的可能是為了保證代碼最大程度上的拓展性
                // 因?yàn)榭梢灾貙慶hildValue()覆蓋
                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++;
            }
        }
    }
}

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

四、ThreadLocalMap原理剖析

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

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

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

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

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();

在案例中,創(chuàng)建了三個ThreadLocal對象:tl0、tl1、tl2以及兩個線程對象:T1、T2,經(jīng)過前面分析我們知道,在每個Thread對象中都維護(hù)著一個ThreadLocalMap類型的成員變量threadlocals存儲每條線程的副本變量。所以,T1、T2內(nèi)部分別都維護(hù)著一個ThreadLocalMap,當(dāng)T1、T2操作tl0、tl1、tl2時,Zero、One、Two都會以key-value的形式存儲在數(shù)組的不同位置,這個數(shù)組就是前面提到的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的哈希值和數(shù)組長度計(jì)算獲取索引值
    int i = key.threadLocalHashCode & (len-1);

    // 遍歷table如果已經(jīng)存在則更新值,不存在則創(chuàng)建
    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]為空則創(chuàng)建新的Entry存儲
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    
    // table[i]不為null且key不相同的情況下,
    // 如果遍歷完數(shù)組也沒有找到為null的位置,
    // 則代表數(shù)組需要擴(kuò)容,則將數(shù)組擴(kuò)容兩倍
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 如果清理過期的數(shù)據(jù)之后,數(shù)組內(nèi)的可用數(shù)據(jù)還占
    // 3/4的情況下,直接擴(kuò)容兩倍
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

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

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

private static AtomicInteger nextHashCode =
    new AtomicInteger();

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

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

因?yàn)?code>ThreadLocal中哈希碼相關(guān)的成員都是靜態(tài)static關(guān)鍵字修飾的原因,每次創(chuàng)建ThreadLocal對象時,都會在對象初始化的時候調(diào)用一次自增方法為ThreadLocal對象生成一個哈希值:

private final int threadLocalHashCode = nextHashCode();

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

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

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

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

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

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

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

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

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

至此整個ThreadLocalMap存儲過程結(jié)束,如下:

ThreadLocalMap存儲原理

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

// ThreadLocal類 -> ThreadLocalMap內(nèi)部類 -> getEntry()方法
private Entry getEntry(ThreadLocal<?> key) {
    // 通過`ThreadLocal`對象的哈希值跟`table`數(shù)組長度
    // 進(jìn)行計(jì)算獲取下標(biāo)索引值`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]之后的元素獲取對應(yīng)key的值
    else
        return getEntryAfterMiss(key, i, e);
}

// ThreadLocal類 -> ThreadLocalMap內(nèi)部類 -> 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相同則返回對應(yīng)的元素
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

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

五、ThreadLocal注意事項(xiàng)

5.1、ThreadLocal線程安全問題

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

5.2、ThreadLocal副本變量的產(chǎn)生

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

5.3、ThreadLocal在線程池情況下可能會產(chǎn)生臟數(shù)據(jù)

因?yàn)榫€程池會復(fù)用線程,而線程上一個執(zhí)行的任務(wù)對ThreadLocal進(jìn)行set()操作后,在線程run()結(jié)束后沒有調(diào)用remove()移除變量副本,下個Runnable任務(wù)如果直接對ThreadLocal進(jìn)行get()操作則可能讀到臟數(shù)據(jù)。

5.4、ThreadLocal可能會造成內(nèi)存泄露

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

因?yàn)閗ey如果不設(shè)計(jì)成弱引用類型的情況下,會造成entry中value出現(xiàn)內(nèi)存泄漏的場景

解決方案

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容