Java并發(fā)學(xué)習(xí)(四)----深入理解ThreadLocal

摘要:

ThreadLocal 又名線程局部變量,是 Java 中一種較為特殊的線程綁定機(jī)制铛铁,用于保證變量在不同線程間的隔離性,以方便每個(gè)線程處理自己的狀態(tài)却妨。進(jìn)一步地避归,本文以ThreadLocal類的源碼為切入點(diǎn),深入分析了ThreadLocal類的作用原理管呵,并給出應(yīng)用場(chǎng)景和一般使用步驟梳毙。


一. 對(duì) ThreadLocal 的理解

1). ThreadLocal 概述

ThreadLocal 又名 線程局部變量 ,是 Java 中一種較為特殊的線程綁定機(jī)制捐下,可以為每一個(gè)使用該變量的線程都提供一個(gè)變量值的副本账锹,并且每一個(gè)線程都可以獨(dú)立地改變自己的副本,而不會(huì)與其它線程的副本發(fā)生沖突坷襟。一般而言奸柬,通過(guò) ThreadLocal 存取的數(shù)據(jù)總是與當(dāng)前線程相關(guān),也就是說(shuō)婴程,JVM 為每個(gè)運(yùn)行的線程綁定了私有的本地實(shí)例存取空間廓奕,從而為多線程環(huán)境常出現(xiàn)的并發(fā)訪問(wèn)問(wèn)題提供了一種 隔離機(jī)制 。

如果一段代碼中所需要的數(shù)據(jù)必須與其他代碼共享档叔,那就看看這些共享數(shù)據(jù)的代碼能否保證在同一個(gè)線程中執(zhí)行桌粉?如果能保證,我們就可以把共享數(shù)據(jù)的可見(jiàn)范圍限制在同一個(gè)線程之內(nèi)衙四,這樣铃肯,無(wú)須同步也能保證線程之間不出現(xiàn)數(shù)據(jù)爭(zhēng)用的問(wèn)題。也就是說(shuō)传蹈,如果一個(gè)某個(gè)變量要被某個(gè)線程 獨(dú)享押逼,那么我們就可以通過(guò)ThreadLocal來(lái)實(shí)現(xiàn)線程本地存儲(chǔ)功能步藕。


2). ThreadLocal 在 JDK 中的定義

ThreadLocal
This class provides thread-local variables. These variables differ from their normal counterparts(副本) in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. 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 ). (如果我們希望通過(guò)將某個(gè)類的狀態(tài)(例如用戶ID、事務(wù)ID)與線程關(guān)聯(lián)起來(lái)挑格,那么通常在這個(gè)類中定義private static類型的ThreadLocal 實(shí)例咙冗。)

Each thread holds an implicit reference to its copy of a thread-local variable (見(jiàn)下圖) as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).

public class Thread implements Runnable {

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

    
}

我們可以從中摘出三條要點(diǎn):

  • 每個(gè)線程都有關(guān)于該 ThreadLocal變量 的私有值
    每個(gè)線程都有一個(gè)獨(dú)立于其他線程的上下文來(lái)保存這個(gè)變量的值,并且對(duì)其他線程是不可見(jiàn)的漂彤。
  • 獨(dú)立于變量的初始值
     ThreadLocal 可以給定一個(gè)初始值雾消,這樣每個(gè)線程就會(huì)獲得這個(gè)初始化值的一個(gè)拷貝,并且每個(gè)線程對(duì)這個(gè)值的修改對(duì)其他線程是不可見(jiàn)的显歧。
  • 將某個(gè)類的狀態(tài)與線程相關(guān)聯(lián)
     我們從JDK中對(duì)ThreadLocal的描述中可以看出,ThreadLocal的一個(gè)重要作用是就是將類的狀態(tài)與線程關(guān)聯(lián)起來(lái)确镊,這個(gè)時(shí)候通常的解決方案就是在這個(gè)類中定義一個(gè) private static ThreadLocal 實(shí)例士骤。

3). 應(yīng)用場(chǎng)景

類 ThreadLocal 主要解決的就是為每個(gè)線程綁定自己的值,以方便其處理自己的狀態(tài)蕾域。形象地講拷肌,可以將 ThreadLocal變量 比喻成全局存放數(shù)據(jù)的盒子,盒子中可以存儲(chǔ)每個(gè)線程的私有數(shù)據(jù)旨巷。例如巨缘,以下類用于生成對(duì)每個(gè)線程唯一的局部標(biāo)識(shí)符。線程 ID 是在第一次調(diào)用 uniqueNum.get() 時(shí)分配的采呐,在后續(xù)調(diào)用中不會(huì)更改若锁。

import java.util.concurrent.atomic.AtomicInteger;

public class UniqueThreadIdGenerator {
    private static final AtomicInteger uniqueId = new AtomicInteger(0);

    private static final ThreadLocal<Integer> uniqueNum = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return uniqueId.getAndIncrement();
        }
    };

    public static void main(String[] args) {
        Thread[] threads = new Thread[5];
        for (int i = 0; i < 5; i++) {
            String name = "Thread-" + i;
            threads[i] = new Thread(name){
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + ": "
                            + uniqueNum.get());
                }
            };
            threads[i].start();
        }

        System.out.println(Thread.currentThread().getName() + ": "
                + uniqueNum.get());
    }
}/* Output(輸出結(jié)果不唯一): 
        Thread-1: 2
        Thread-0: 0
        Thread-2: 3
        main: 1
        Thread-3: 4
        Thread-4: 5
 *///:~

二. 深入分析ThreadLocal類

下面,我們來(lái)看一下 ThreadLocal 的具體實(shí)現(xiàn)斧吐,該類一共提供的四個(gè)方法:

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

其中又固,get()方法是用來(lái)獲取 ThreadLocal變量 在當(dāng)前線程中保存的值,set() 用來(lái)設(shè)置 ThreadLocal變量 在當(dāng)前線程中的值煤率,remove() 用來(lái)移除當(dāng)前線程中相關(guān) ThreadLocal變量仰冠,initialValue() 是一個(gè) protected 方法,一般需要重寫蝶糯。

1洋只、 原理探究

1). 切入點(diǎn):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();    // 獲取當(dāng)前線程對(duì)象
        ThreadLocalMap map = getMap(t);     // 獲取當(dāng)前線程的成員變量 threadLocals
        if (map != null) {
            // 從當(dāng)前線程的 ThreadLocalMap 獲取該 thread-local variable 對(duì)應(yīng)的 entry
            ThreadLocalMap.Entry e = map.getEntry(this);    
            if (e != null)      
                return (T)e.value;   // 取得目標(biāo)值
        }
        return setInitialValue();  
    }

2).關(guān)鍵點(diǎn):setInitialValue()

/**
     * 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();     // 默認(rèn)實(shí)現(xiàn)返回 null
        Thread t = Thread.currentThread();   // 獲得當(dāng)前線程
        ThreadLocalMap map = getMap(t);     // 得到當(dāng)前線程 ThreadLocalMap類型域 threadLocals
        if (map != null)
            map.set(this, value);  // 該 map 的鍵是當(dāng)前 ThreadLocal 對(duì)象
        else
            createMap(t, value);   
        return value;
    }

我們緊接著看上述方法涉及到的三個(gè)方法:initialValue()昼捍,set(this, value) 和 createMap(t, value)识虚。

(1) initialValue()

   /**
     * 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 <tt>initialValue</tt> 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 <tt>null</tt>; if the
     * programmer desires thread-local variables to have an initial
     * value other than <tt>null</tt>, <tt>ThreadLocal</tt> 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;            // 默認(rèn)實(shí)現(xiàn)返回 null
    }

(2) 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
     * @param map the map to store.
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue); // this 指代當(dāng)前 ThreadLocal 變量,為 map 的鍵  
    }

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

① 每個(gè)線程內(nèi)部有一個(gè) ThreadLocal.ThreadLocalMap 類型的成員變量 threadLocals舷礼,這個(gè) threadLocals 存儲(chǔ)了與該線程相關(guān)的所有 ThreadLocal 變量及其對(duì)應(yīng)的值(”ThreadLocal 變量及其對(duì)應(yīng)的值” 就是該Map中的一個(gè) Entry)。我們知道郊闯,Map 中存放的是一個(gè)個(gè) Entry妻献,其中每個(gè) Entry 都包含一個(gè) Key 和一個(gè) Value蛛株。在這里,就threadLocals 而言育拨,它的 Entry 的 Key 是 ThreadLocal 變量谨履, Value 是該 ThreadLocal 變量對(duì)應(yīng)的值;

② 初始時(shí)熬丧,在Thread里面笋粟,threadLocals為空,當(dāng)通過(guò)ThreadLocal變量調(diào)用get()方法或者set()方法析蝴,就會(huì)對(duì)Thread類中的threadLocals進(jìn)行初始化害捕,并且以當(dāng)前ThreadLocal變量為鍵值,以ThreadLocal要保存的值為value闷畸,存到 threadLocals尝盼;

③ 然后在當(dāng)前線程里面,如果要使用ThreadLocal對(duì)象佑菩,就可以通過(guò)get方法獲得該線程的threadLocals盾沫,然后以該ThreadLocal對(duì)象為鍵取得其對(duì)應(yīng)的 Value,也就是ThreadLocal對(duì)象中所存儲(chǔ)的值殿漠。


2赴精、實(shí)例驗(yàn)證

下面通過(guò)一個(gè)例子來(lái)證明通過(guò)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("父線程 main :");
        System.out.println(test.getLong());
        System.out.println(test.getString());

        Thread thread1 = new Thread() {
            public void run() {
                test.set();
                System.out.println("\n子線程 Thread-0 :");
                System.out.println(test.getLong());
                System.out.println(test.getString());
            };
        };
        thread1.start();
    }
}/* Output: 
        父線程 main :
                    1
                    main

        子線程 Thread-0 :
                    12
                    Thread-0
 *///:~

從這段代碼的輸出結(jié)果可以看出,在main線程中和thread1線程中绞幌,longLocal保存的副本值和stringLocal保存的副本值都不一樣蕾哟,并且進(jìn)一步得出:

實(shí)際上,通過(guò) 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就能正常訪問(wèn)的話鞠眉,必須重寫initialValue()方法。


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

在 Java 中择诈,類 ThreadLocal 解決的是變量在不同線程間的隔離性械蹋。最常見(jiàn)的 ThreadLocal 使用場(chǎng)景有 數(shù)據(jù)庫(kù)連接問(wèn)題、Session管理等羞芍。

(1) 數(shù)據(jù)庫(kù)連接問(wèn)題

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

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

(2) Session管理

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

(3) Thread-per-Request (一個(gè)請(qǐng)求對(duì)應(yīng)一個(gè)服務(wù)器線程)

在經(jīng)典Web交互模型中哗戈,請(qǐng)求的處理基本上采用的都是“一個(gè)請(qǐng)求對(duì)應(yīng)一個(gè)服務(wù)器線程”的處理方式,因此就可以將請(qǐng)求設(shè)置成類似ThreadLocal<Request>的形式荷科,這樣唯咬,當(dāng)某個(gè)服務(wù)器線程來(lái)處理請(qǐng)求時(shí)纱注,就可以獨(dú)享該請(qǐng)求的處理了。


四. ThreadLocal 一般使用步驟

ThreadLocal 使用步驟一般分為三步:

  • 創(chuàng)建一個(gè) ThreadLocal 對(duì)象 threadXxx胆胰,用來(lái)保存線程間需要隔離處理的對(duì)象 xxx狞贱;
  • 提供一個(gè)獲取要隔離訪問(wèn)的數(shù)據(jù)的方法 getXxx(),在方法中判斷蜀涨,若 ThreadLocal對(duì)象為null時(shí)候瞎嬉,應(yīng)該 new() 一個(gè)隔離訪問(wèn)類型的對(duì)象;
  • 在線程類的run()方法中厚柳,通過(guò)getXxx()方法獲取要操作的數(shù)據(jù)氧枣,這樣可以保證每個(gè)線程對(duì)應(yīng)一個(gè)數(shù)據(jù)對(duì)象,在任何時(shí)刻都操作的是這個(gè)對(duì)象别垮,不會(huì)交叉便监。

引用(本文章只供本人學(xué)習(xí)以及學(xué)習(xí)的記錄,如有侵權(quán)宰闰,請(qǐng)聯(lián)系我刪除)

Java 并發(fā):深入理解 ThreadLocal

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末茬贵,一起剝皮案震驚了整個(gè)濱河市簿透,隨后出現(xiàn)的幾起案子移袍,更是在濱河造成了極大的恐慌,老刑警劉巖老充,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件葡盗,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡啡浊,警方通過(guò)查閱死者的電腦和手機(jī)觅够,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)巷嚣,“玉大人喘先,你說(shuō)我怎么就攤上這事⊥⒘#” “怎么了窘拯?”我有些...
    開(kāi)封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)坝茎。 經(jīng)常有香客問(wèn)我涤姊,道長(zhǎng),這世上最難降的妖魔是什么嗤放? 我笑而不...
    開(kāi)封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任思喊,我火速辦了婚禮,結(jié)果婚禮上次酌,老公的妹妹穿的比我還像新娘恨课。我一直安慰自己舆乔,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布庄呈。 她就那樣靜靜地躺著蜕煌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪诬留。 梳的紋絲不亂的頭發(fā)上斜纪,一...
    開(kāi)封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音文兑,去河邊找鬼盒刚。 笑死,一個(gè)胖子當(dāng)著我的面吹牛绿贞,可吹牛的內(nèi)容都是我干的因块。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼籍铁,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼涡上!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起拒名,我...
    開(kāi)封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤吩愧,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后增显,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體雁佳,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年同云,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了糖权。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡炸站,死狀恐怖星澳,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情旱易,我是刑警寧澤禁偎,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站咒唆,受9級(jí)特大地震影響届垫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜全释,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一装处、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦妄迁、人聲如沸寝蹈。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)箫老。三九已至,卻和暖如春黔州,著一層夾襖步出監(jiān)牢的瞬間耍鬓,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工流妻, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留牲蜀,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓绅这,卻偏偏與公主長(zhǎng)得像涣达,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子证薇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容