Java多線程之ThreadLocal剖析

對于ThreadLocal這個類我相信多數(shù)java攻城獅都不陌生冠摄,它是java多線程中的一個重要的類纵搁,在面試時經(jīng)常被問到。不知道大家有沒有和我一樣的感覺佃迄,雖然大概知道這個類的含義但對其實現(xiàn)機制以及什么時候該使用這個類似乎總是理解的不太深刻。如果你和我有同樣的困惑贵少,就一起來看看這篇文章呵俏。

1.ThreadLocal是什么

看看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可以為線程提供專屬本線程的變量
  • 變量通過ThreadLocal的get、set方法訪問
  • 線程間變量相互獨立
  • ThreadLocal對象一般定義為一個私有靜態(tài)變量

OK, 通過這幾個點我們知道了ThreadLocal是用來做什么的滔灶。ThreadLocal為我們提供了一種將多線程共享變量變?yōu)榫€程獨享變量的一種機制普碎,通過ThreadLocal可以將共享變量變?yōu)閷俦揪€程的獨占變量,線程之間可以自由操作ThreadLocal變量录平,互不影響麻车。我們知道了ThreadLocal能干什么,我們再來看看它是怎么實現(xiàn)的斗这。

2.ThreadLocal實現(xiàn)原理

ThreadLocal中最常用的API就是set和get了动猬,set用于給ThreadLocal對象設(shè)置值,get用于從ThreadLocal對象中取值涝影。

private static ThreadLocal<Connection> connectionThreadLocal = new ThreadLocal<Connection>();

    public static void main(String[] args) {
        if (connectionThreadLocal.get() == null){
            connectionThreadLocal.set(ConnectionUtil.getConnection());
        }
        Thread thread = new Thread(new ConnectionTask(connectionThreadLocal));
        thread.start();
        try {
            //等子線程執(zhí)行結(jié)束后枣察,再繼續(xù)執(zhí)行主線程
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Connection connection = connectionThreadLocal.get();
        System.out.println(Thread.currentThread().getName() + " : " + connection);
    }

    static class ConnectionTask implements Runnable{

        private ThreadLocal<Connection> threadLocal;

        public ConnectionTask(ThreadLocal<Connection> threadLocal){
            this.threadLocal = threadLocal;
        }
        public void run() {
            Connection connection = threadLocal.get();
            if (connection == null){
                connection = ConnectionUtil.getConnection();
                threadLocal.set(connection);
            }
            System.out.println(Thread.currentThread().getName() +" : " + connection);
        }
    }

上面的代碼中在主線程和子線程中分別對同一個ThreadLocal對象執(zhí)行set操作争占,并分別執(zhí)行g(shù)et方法取出ThreadLocal對象中的Connnection對象燃逻。主線程的get操作在子線程全部執(zhí)行完后才繼續(xù)執(zhí)行。大家猜測一下兩個線程中執(zhí)行打印后輸出的是否是同一個Connection對象呢?

代碼執(zhí)行結(jié)果

可以看到兩個線程中輸出的是兩個connection對象臂痕。這是為什么呢伯襟?要知道我們在兩個線程中操作的可是同一個ThreadLocal對象啊,假如我們將ThreadLocal當做普通的Bean握童,它在主線程中執(zhí)行set操作后在子線程中對該對象再執(zhí)行g(shù)et應(yīng)該取到的是在主線程中set的值姆怪,可是我們看到的結(jié)果并不是如此。我們通過ThreadLocal的get澡绩、set源碼來看看為什么會是這樣的結(jié)果稽揭。

先來看get方法:

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

在get方法中首先拿到執(zhí)行方法的當前線程t,然后以線程t為參數(shù)調(diào)用了getMap(t)方法返回了一個ThreadLocalMap對象肥卡。如果map對象不為null就從map返回result溪掀,如果為null就返回setInitialValue()。有一點比較費解的是map.getEntry傳入的key是this步鉴,也就是當前ThreadLocal對象揪胃,我們來逐層分析璃哟,一點點揭開ThreadLocal的神秘面紗。關(guān)鍵點在于這個ThreadLocalMap對象喊递,我們來看看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返回的是當前線程中一個threadLocals屬性,線程的threadLocals屬性是啥呢骚勘。

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

這是Thread類中的threadLocals屬性铐伴,從注釋上我們可以看到這個threadLocals用來維護屬于這個線程的ThreadLocal對象,而ThreadLocalMap是ThreadLocal的一個內(nèi)部類俏讹。既然ThreadLocalMap是一個Map盛杰,那我們?nèi)タ匆幌滤膋ey、value分別是什么藐石。

         /**
         * 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;
            }
        }

這是ThreadLocal的Entry定義即供,可以看到ThreadLocalMap的key是一個ThreadLocal對象,其value是一個Object于微。

看到這里我們應(yīng)該能夠知道的是:執(zhí)行ThreadLocal的get方法會返回屬于當前執(zhí)行線程的變量逗嫡,該變量保存在線程的ThreadLocalMap屬性中。Map以當前ThreadLocal對象為key株依,value是一個Object對象驱证,這個value就是在set到ThreadLocal中的那個值。

再看一下set方法:

/**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

可以看到ThreadLocal的set方法同樣是先獲取當前線程恋腕,然后拿到當前線程的ThreadLocalMap引用map抹锄。如果map不為null,就以當前threadLocal對象為key荠藤,T value作為值保存到線程的threadLocals屬性中伙单。否則執(zhí)行createMap()方法。

/**
     * 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);
    }

createMap方法很簡單哈肖,就是初始化線程的threadLocals屬性吻育,并將第一個通過ThreadLocal set的值保存到threadLocals中。

看到這里ThreadLocal為線程提供線程本地變量的機制也就很清楚了

  • 通過ThreadLocal的set方法設(shè)置的value實際上被保存到了當前線程的ThreadLocalMap對象中淤井,Map以ThreadLocal對象為key布疼,保存的值為value
  • 在調(diào)用ThreadLocal的get時,實際上是以當前的ThreadLocal對象為key從當前線程的ThreadLocalMap屬性中取出對應(yīng)的value

這也就解釋了在本節(jié)開頭中那個demo的輸出:同一個ThreadLocal對象币狠,分別在兩個線程中執(zhí)行set方法后游两,再調(diào)用get方法輸出的是兩個不同的對象。因為主線程和子線程中的分別擁有各自的ThreadLocalMap屬性來保存<ThreadLocal this,T value>的鍵值對漩绵,因此兩個線程的中value是互不相干的贱案。

3.什么時候使用ThreadLocal

當訪問共享變量時為了確保并發(fā)安全性,通常需要線程間同步渐行。我們知道線程間同步是非常麻煩且耗費性能的轰坊。一種避免線程間同步的就是不進行線程間數(shù)據(jù)共享铸董,僅在線程內(nèi)訪問數(shù)據(jù)就無需進行線程同步了,這種技術(shù)被稱為線程封閉肴沫。而ThreadLocal就是一種非常好的實現(xiàn)線程封閉的技術(shù)粟害。

當我們不希望線程間共享變量時,或者說線程完全沒有必要進行變量共享時就可以使用ThreadLocal對象將變量設(shè)置為線程獨享的變量颤芬。

例如悲幅,在單線程應(yīng)用程序中可能會維持一個全局的數(shù)據(jù)庫連接,并在程序啟動的時候初始化這個連接對象站蝠,從而避免在調(diào)用每個方法時都初始化都要傳遞一個Connection對象汰具。但是在多線程中Connection對象是共享的,沒有線程同步的情況下這是不安全的菱魔×衾螅可能一個線程正在使用共享的Connection對象,而另一個線程就將Connection對象close了澜倦。利用ThreadLocal則可以為每個線程都初始化一個Connection對象聚蝶,每個線程獨立操作屬于本線程的Connection,互不影響藻治。

private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){
        @Override
        protected Connection initialValue() {
            return DriverManager.getConnection(DB_URL);
        }
    };

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

最后碘勉,需要注意的是在使用ThreadLocal時,一般將ThreadLocal設(shè)置私有靜態(tài)變量桩卵,get前先set或者重寫initialValue方法验靡。如果有多個對象需要set到ThreadLocal中,就初始化多個ThreadLocal變量雏节。

好了胜嗓,ThreadLocal的介紹就到這里。

你的關(guān)注是我持續(xù)更新的動力矾屯!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末兼蕊,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子件蚕,更是在濱河造成了極大的恐慌,老刑警劉巖产禾,帶你破解...
    沈念sama閱讀 216,997評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件排作,死亡現(xiàn)場離奇詭異,居然都是意外死亡亚情,警方通過查閱死者的電腦和手機妄痪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來楞件,“玉大人衫生,你說我怎么就攤上這事裳瘪。” “怎么了罪针?”我有些...
    開封第一講書人閱讀 163,359評論 0 353
  • 文/不壞的土叔 我叫張陵彭羹,是天一觀的道長。 經(jīng)常有香客問我泪酱,道長派殷,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,309評論 1 292
  • 正文 為了忘掉前任墓阀,我火速辦了婚禮毡惜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘斯撮。我一直安慰自己经伙,他們只是感情好,可當我...
    茶點故事閱讀 67,346評論 6 390
  • 文/花漫 我一把揭開白布勿锅。 她就那樣靜靜地躺著橱乱,像睡著了一般。 火紅的嫁衣襯著肌膚如雪粱甫。 梳的紋絲不亂的頭發(fā)上泳叠,一...
    開封第一講書人閱讀 51,258評論 1 300
  • 那天,我揣著相機與錄音茶宵,去河邊找鬼危纫。 笑死,一個胖子當著我的面吹牛乌庶,可吹牛的內(nèi)容都是我干的种蝶。 我是一名探鬼主播,決...
    沈念sama閱讀 40,122評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼瞒大,長吁一口氣:“原來是場噩夢啊……” “哼螃征!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起透敌,我...
    開封第一講書人閱讀 38,970評論 0 275
  • 序言:老撾萬榮一對情侶失蹤盯滚,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后酗电,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體魄藕,經(jīng)...
    沈念sama閱讀 45,403評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,596評論 3 334
  • 正文 我和宋清朗相戀三年撵术,在試婚紗的時候發(fā)現(xiàn)自己被綠了背率。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,769評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖寝姿,靈堂內(nèi)的尸體忽然破棺而出交排,到底是詐尸還是另有隱情,我是刑警寧澤饵筑,帶...
    沈念sama閱讀 35,464評論 5 344
  • 正文 年R本政府宣布埃篓,位于F島的核電站,受9級特大地震影響翻翩,放射性物質(zhì)發(fā)生泄漏都许。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,075評論 3 327
  • 文/蒙蒙 一嫂冻、第九天 我趴在偏房一處隱蔽的房頂上張望胶征。 院中可真熱鬧,春花似錦桨仿、人聲如沸睛低。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽钱雷。三九已至,卻和暖如春吹零,著一層夾襖步出監(jiān)牢的瞬間罩抗,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評論 1 269
  • 我被黑心中介騙來泰國打工灿椅, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留套蒂,地道東北人。 一個月前我還...
    沈念sama閱讀 47,831評論 2 370
  • 正文 我出身青樓茫蛹,卻偏偏與公主長得像操刀,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子婴洼,可洞房花燭夜當晚...
    茶點故事閱讀 44,678評論 2 354