ThreadLocal詳解分析

前言

要了解ThreadLocal,首先搞清楚ThreadLocal 是什么?是用來解決什么問題的?
ThreadLocal 是線程的局部變量, 是每一個(gè)線程所單獨(dú)持有的,其他線程不能對(duì)其進(jìn)行訪問, 通常是類中的 private static 字段,是對(duì)該字段初始值的一個(gè)拷貝,它們希望將狀態(tài)與某一個(gè)線程(例如,用戶 ID 或事務(wù) ID)相關(guān)聯(lián)

我們知道有時(shí)候一個(gè)對(duì)象的變量會(huì)被多個(gè)線程所訪問,這時(shí)就會(huì)有線程安全問題,當(dāng)然我們可以使用synchorinized 關(guān)鍵字來為此變量加鎖,進(jìn)行同步處理,從而限制只能有一個(gè)線程來使用此變量,但是加鎖會(huì)大大影響程序執(zhí)行效率,此外我們還可以使用ThreadLocal來解決對(duì)某一個(gè)變量的訪問沖突問題。

當(dāng)使用ThreadLocal維護(hù)變量的時(shí)候 為每一個(gè)使用該變量的線程提供一個(gè)獨(dú)立的變量副本,即每個(gè)線程內(nèi)部都會(huì)有一個(gè)該變量,這樣同時(shí)多個(gè)線程訪問該變量并不會(huì)彼此相互影響,因此他們使用的都是自己從內(nèi)存中拷貝過來的變量的副本, 這樣就不存在線程安全問題,也不會(huì)影響程序的執(zhí)行性能。


使用場(chǎng)景

ThreadLocal,很多地方叫做線程本地變量,也有些地方叫做線程本地存儲(chǔ),其實(shí)意思差不多??赡芎芏嗯笥讯贾繲hreadLocal為變量在每個(gè)線程中都創(chuàng)建了一個(gè)副本,那么每個(gè)線程可以訪問自己內(nèi)部的副本變量。

這句話從字面上看起來很容易理解,但是真正理解并不是那么容易。

我們還是先來看一個(gè)例子:

class ConnectionManager {
     
    private static Connection connect = null;
     
    public static Connection openConnection() {
        if(connect == null){
            connect = DriverManager.getConnection();
        }
        return connect;
    }
     
    public static void closeConnection() {
        if(connect!=null)
            connect.close();
    }
}

假設(shè)有這樣一個(gè)數(shù)據(jù)庫鏈接管理類,這段代碼在單線程中使用是沒有任何問題的,但是如果在多線程中使用呢?很顯然,在多線程中使用會(huì)存在線程安全問題:

  • 第一,這里面的2個(gè)方法都沒有進(jìn)行同步,很可能在openConnection方法中會(huì)多次創(chuàng)建connect;
  • 第二,由于connect是共享變量,那么必然在調(diào)用connect的地方需要使用到同步來保障線程安全,因?yàn)楹芸赡芤粋€(gè)線程在使用connect進(jìn)行數(shù)據(jù)庫操作,而另外一個(gè)線程調(diào)用closeConnection關(guān)閉鏈接。

所以出于線程安全的考慮,必須將這段代碼的兩個(gè)方法進(jìn)行同步處理,并且在調(diào)用connect的地方需要進(jìn)行同步處理。

這樣將會(huì)大大影響程序執(zhí)行效率,因?yàn)橐粋€(gè)線程在使用connect進(jìn)行數(shù)據(jù)庫操作的時(shí)候,其他線程只有等待。

那么大家來仔細(xì)分析一下這個(gè)問題,這地方到底需不需要將connect變量進(jìn)行共享?事實(shí)上,是不需要的。假如每個(gè)線程中都有一個(gè)connect變量,各個(gè)線程之間對(duì)connect變量的訪問實(shí)際上是沒有依賴關(guān)系的,即一個(gè)線程不需要關(guān)心其他線程是否對(duì)這個(gè)connect進(jìn)行了修改的。

到這里,可能會(huì)有朋友想到,既然不需要在線程之間共享這個(gè)變量,可以直接這樣處理,在每個(gè)需要使用數(shù)據(jù)庫連接的方法中具體使用時(shí)才創(chuàng)建數(shù)據(jù)庫鏈接,然后在方法調(diào)用完畢再釋放這個(gè)連接。比如下面這樣:

class ConnectionManager {
     
    private  Connection connect = null;
     
    public Connection openConnection() {
        if(connect == null){
            connect = DriverManager.getConnection();
        }
        return connect;
    }
     
    public void closeConnection() {
        if(connect!=null)
            connect.close();
    }
}
 
 
class Dao{
    public void insert() {
        ConnectionManager connectionManager = new ConnectionManager();
        Connection connection = connectionManager.openConnection();
         
        //使用connection進(jìn)行操作
         
        connectionManager.closeConnection();
    }
}

這樣處理確實(shí)也沒有任何問題,由于每次都是在方法內(nèi)部創(chuàng)建的連接,那么線程之間自然不存在線程安全問題。但是這樣會(huì)有一個(gè)致命的影響:導(dǎo)致服務(wù)器壓力非常大,并且嚴(yán)重影響程序執(zhí)行性能。由于在方法中需要頻繁地開啟和關(guān)閉數(shù)據(jù)庫連接,這樣不盡嚴(yán)重影響程序執(zhí)行效率,還可能導(dǎo)致服務(wù)器壓力巨大。

那么這種情況下使用ThreadLocal是再適合不過的了,因?yàn)門hreadLocal在每個(gè)線程中對(duì)該變量會(huì)創(chuàng)建一個(gè)副本,即每個(gè)線程內(nèi)部都會(huì)有一個(gè)該變量,且在線程內(nèi)部任何地方都可以使用,線程之間互不影響,這樣一來就不存在線程安全問題,也不會(huì)嚴(yán)重影響程序執(zhí)行性能。

但是要注意,雖然ThreadLocal能夠解決上面說的問題,但是由于在每個(gè)線程中都創(chuàng)建了副本,所以要考慮它對(duì)資源的消耗,比如內(nèi)存的占用會(huì)比不使用ThreadLocal要大。


深入解析

在上面談到了對(duì)ThreadLocal的一些理解,那我們下面來看一下具體ThreadLocal是如何實(shí)現(xiàn)的。

先了解一下ThreadLocal類提供的幾個(gè)方法:

public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }

get()方法是用來獲取ThreadLocal在當(dāng)前線程中保存的變量副本,set()用來設(shè)置當(dāng)前線程中變量的副本,remove()用來移除當(dāng)前線程中變量的副本,initialValue()是一個(gè)protected方法,一般是用來在使用時(shí)進(jìn)行重寫的,它是一個(gè)延遲加載方法,下面會(huì)詳細(xì)說明。

首先我們來看一下ThreadLocal類是如何為每個(gè)線程創(chuàng)建一個(gè)變量的副本的。

先看下get方法的實(shí)現(xiàn):

    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    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();
    }

第一句是取得當(dāng)前線程,然后通過getMap(t)方法獲取到一個(gè)map,map的類型為ThreadLocalMap。然后接著下面獲取到< key,value >鍵值對(duì),注意這里獲取鍵值對(duì)傳進(jìn)去的是 this,而不是當(dāng)前線程t。

如果獲取成功,則返回value值。

如果map為空,則調(diào)用setInitialValue方法返回value。

我們上面的每一句來仔細(xì)分析:

首先看一下getMap方法中做了什么:

    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

可能大家沒有想到的是,在getMap中,是調(diào)用當(dāng)期線程t,返回當(dāng)前線程t中的一個(gè)成員變量threadLocals。

那么我們繼續(xù)取Thread類中取看一下成員變量threadLocals是什么:

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

實(shí)際上就是一個(gè)ThreadLocalMap,這個(gè)類型是ThreadLocal類的一個(gè)內(nèi)部類,我們繼續(xù)取看ThreadLocalMap的實(shí)現(xiàn):

    static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

可以看到ThreadLocalMap的Entry繼承了WeakReference,并且使用ThreadLocal作為鍵值。

然后再繼續(xù)看setInitialValue方法的具體實(shí)現(xiàn):

    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial 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;
    }

很容易了解,就是如果map不為空,就設(shè)置鍵值對(duì),為空,再創(chuàng)建Map,看一下createMap的實(shí)現(xiàn):

    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

至此,可能大部分朋友已經(jīng)明白了ThreadLocal是如何為每個(gè)線程創(chuàng)建變量的副本的:

首先,在每個(gè)線程Thread內(nèi)部有一個(gè)ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個(gè)threadLocals就是用來存儲(chǔ)實(shí)際的變量副本的,鍵值為當(dāng)前ThreadLocal變量,value為變量副本(即T類型的變量)。

初始時(shí),在Thread里面,threadLocals為空,當(dāng)通過ThreadLocal變量調(diào)用get()方法或者set()方法,就會(huì)對(duì)Thread類中的threadLocals進(jìn)行初始化,并且以當(dāng)前ThreadLocal變量為鍵值,以ThreadLocal要保存的副本變量為value,存到threadLocals。

然后在當(dāng)前線程里面,如果要使用副本變量,就可以通過get方法在threadLocals里面查找。

下面通過一個(gè)例子來證明通過ThreadLocal能達(dá)到在每個(gè)線程中創(chuàng)建變量副本的效果:

public class Test {
    ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
    ThreadLocal<String> stringLocal = new ThreadLocal<String>();
 
     
    public void set() {
        longLocal.set(Thread.currentThread().getId());
        stringLocal.set(Thread.currentThread().getName());
    }
     
    public long getLong() {
        return longLocal.get();
    }
     
    public String getString() {
        return stringLocal.get();
    }
     
    public static void main(String[] args) throws InterruptedException {
        final Test test = new Test();
         
         
        test.set();
        System.out.println(test.getLong());
        System.out.println(test.getString());
     
         
        Thread thread1 = new Thread(){
            public void run() {
                test.set();
                System.out.println(test.getLong());
                System.out.println(test.getString());
            };
        };
        thread1.start();
        thread1.join();
         
        System.out.println(test.getLong());
        System.out.println(test.getString());
    }
}

這段代碼的輸出結(jié)果為:

1
main
8
Thread-0
1
main

從這段代碼的輸出結(jié)果可以看出,在main線程中和thread1線程中,longLocal保存的副本值和stringLocal保存的副本值都不一樣。最后一次在main線程再次打印副本值是為了證明在main線程中和thread1線程中的副本值確實(shí)是不同的。

總結(jié)一下:

  • 實(shí)際的通過ThreadLocal創(chuàng)建的副本是存儲(chǔ)在每個(gè)線程自己的threadLocals中的;

  • 為何threadLocals的類型ThreadLocalMap的鍵值為ThreadLocal對(duì)象,因?yàn)槊總€(gè)線程中可有多個(gè)threadLocal變量,就像上面代碼中的longLocal和stringLocal;

  • 在進(jìn)行g(shù)et之前,必須先set,否則會(huì)報(bào)空指針異常;

如果想在get之前不需要調(diào)用set就能正常訪問的話,必須重寫initialValue()方法。

因?yàn)樵谏厦娴拇a分析過程中,我們發(fā)現(xiàn)如果沒有先set的話,即在map中查找不到對(duì)應(yīng)的存儲(chǔ),則會(huì)通過調(diào)用setInitialValue方法返回i,而在setInitialValue方法中,有一個(gè)語句是T value = initialValue(), 而默認(rèn)情況下,initialValue方法返回的是null。

    /**
     * Returns the current thread's "initial value" for this
     * thread-local variable.  This method will be invoked the first
     * time a thread accesses the variable with the {@link #get}
     * method, unless the thread previously invoked the {@link #set}
     * method, in which case the {@code initialValue} method will not
     * be invoked for the thread.  Normally, this method is invoked at
     * most once per thread, but it may be invoked again in case of
     * subsequent invocations of {@link #remove} followed by {@link #get}.
     *
     * <p>This implementation simply returns {@code null}; if the
     * programmer desires thread-local variables to have an initial
     * value other than {@code null}, {@code ThreadLocal} must be
     * subclassed, and this method overridden.  Typically, an
     * anonymous inner class will be used.
     *
     * @return the initial value for this thread-local
     */
    protected T initialValue() {
        return null;
    }

ThreadLocal的應(yīng)用場(chǎng)景

最常見的ThreadLocal使用場(chǎng)景為 用來解決 數(shù)據(jù)庫連接、Session管理等。

如:

private static ThreadLocal<Connection> connectionHolder
= new ThreadLocal<Connection>() {
public Connection initialValue() {
    return DriverManager.getConnection(DB_URL);
}
};
 
public static Connection getConnection() {
return connectionHolder.get();
}
private static final ThreadLocal threadSession = new ThreadLocal();
 
public static Session getSession() throws InfrastructureException {
    Session s = (Session) threadSession.get();
    try {
        if (s == null) {
            s = getSessionFactory().openSession();
            threadSession.set(s);
        }
    } catch (HibernateException ex) {
        throw new InfrastructureException(ex);
    }
    return s;
}

PS

關(guān)于ThreadLocalMap< ThreadLocal, Object >弱引用問題

當(dāng)線程沒有結(jié)束,但是ThreadLocal已經(jīng)被回收,則可能導(dǎo)致線程中存在ThreadLocalMap< null, Object >的鍵值對(duì),造成內(nèi)存泄露。(ThreadLocal被回收,ThreadLocal關(guān)聯(lián)的線程共享變量還存在)。

雖然ThreadLocal的get,set方法可以清除ThreadLocalMap中key為null的value,但是get,set方法在內(nèi)存泄露后并不會(huì)必然調(diào)用,所以為了防止此類情況的出現(xiàn),我們有兩種手段。

1、使用完線程共享變量后,顯示調(diào)用ThreadLocalMap.remove方法清除線程共享變量;

2、JDK建議ThreadLocal定義為private static,這樣ThreadLocal的弱引用問題則不存在了。


總結(jié)

  • 每個(gè)線程都有一個(gè)ThreadLocalMap 類型的 threadLocals 屬性。
  • ThreadLocalMap 類相當(dāng)于一個(gè)Map,key 是 ThreadLocal 本身,value 就是我們的值。
  • 當(dāng)我們通過 threadLocal.set(new Integer(123)); ,我們就會(huì)在這個(gè)線程中的 threadLocals 屬性中放入一個(gè)鍵值對(duì),key 是 這個(gè) threadLocal.set(new Integer(123))的threadlocal,value 就是值new Integer(123)。
  • 當(dāng)我們通過 threadlocal.get() 方法的時(shí)候,首先會(huì)根據(jù)這個(gè)線程得到這個(gè)線程的 threadLocals 屬性,然后由于這個(gè)屬性放的是鍵值對(duì),我們就可以根據(jù)鍵 threadlocal 拿到值。 注意,這時(shí)候這個(gè)鍵 threadlocal 和 我們 set 方法的時(shí)候的那個(gè)鍵 threadlocal 是一樣的,所以我們能夠拿到相同的值。
  • ThreadLocalMap 的get/set/remove方法跟HashMap的內(nèi)部實(shí)現(xiàn)都基本一樣,通過 "key.threadLocalHashCode & (table.length - 1)" 運(yùn)算式計(jì)算得到我們想要找的索引位置,如果該索引位置的鍵值對(duì)不是我們要找的,則通過nextIndex方法計(jì)算下一個(gè)索引位置,直到找到目標(biāo)鍵值對(duì)或者為空。
  • hash沖突:在HashMap中相同索引位置的元素以鏈表形式保存在同一個(gè)索引位置;而在ThreadLocalMap中,沒有使用鏈表的數(shù)據(jù)結(jié)構(gòu),而是將(當(dāng)前的索引位置+1)對(duì)length取模的結(jié)果作為相同索引元素的位置:源碼中的nextIndex方法,可以表達(dá)成如下公式:如果i為當(dāng)前索引位置,則下一個(gè)索引位置 = (i + 1 < len) ? i + 1 : 0。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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