(10)ThreadLocal

什么是ThreadLocal

ThreadLocal是一個用于創(chuàng)建線程局部變量的類衰抑,它有兩個特點愕撰,其一對于線程A創(chuàng)建的數(shù)據(jù)只有線程A能獲取和修改;其二只要能獲取到ThreadLocal的示例,線程就能獲取到其中的值不翩。

使用場景

網(wǎng)上有介紹ThreadLocal與synchronized對比的文章,但是我覺得它們之間并沒有可以性麻裳。ThreadLocal注重的點在于通過線程獨有空間來存儲和獲取數(shù)據(jù)口蝠,而synchronized注重的是多線程同時對同一變量的獲取與修改。

簡單的比喻是ThreadLocal好比每個人(線程)有自己的口袋存津坑,每個人只通過自己口袋來存放和取東西妙蔗。而synchronized就好比所有人(多有線程)都在同一個口袋存放和取東西,但是一次只能一個人操作疆瑰。

public class App6 {
    public static Map<Thread,String> globalMap = new HashMap<>();
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                Thread currentThread = Thread.currentThread();
                //存數(shù)據(jù)
                globalMap.put(currentThread,"abc");
                //取數(shù)據(jù)
                System.out.printf("[%s]=[%s]\n",currentThread.getName(),globalMap.get(currentThread));
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                Thread currentThread = Thread.currentThread();
                //存數(shù)據(jù)
                globalMap.put(currentThread,"123");
                //取數(shù)據(jù)
                System.out.printf("[%s]=[%s]\n",currentThread.getName(),globalMap.get(currentThread));
            }
        }).start();
        TimeUnit.SECONDS.sleep(1);
        System.out.println(globalMap);
    }
}

上面的示例中眉反,每個線程將自己的數(shù)據(jù)放在Map中,然后獲取自己數(shù)據(jù)穆役。但是上面的示例是線程不安全的寸五,千萬不要在自己的代碼中使用。如果使用上面的方案耿币,我們需要使用線程安全的Map或者加鎖來解決梳杏。即使解決了線程安全的問題還存在兩個問題,其一就是Map中的值并不是當(dāng)前線程獨有的淹接,其他線程也是可以獲取和修改它十性。其二為了保證線程安全Map需要加鎖,在性能上時有損失的塑悼。
上面所提到的問題使用ThreadLocal都可以解決【⑹剩現(xiàn)在修改代碼如下:

public class App7 {
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                Thread currentThread = Thread.currentThread();
                //存數(shù)據(jù)
                threadLocal.set("abc");
                //取數(shù)據(jù)
                System.out.printf("[%s]=[%s]\n",currentThread.getName(),threadLocal.get());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                Thread currentThread = Thread.currentThread();
                //存數(shù)據(jù)
                threadLocal.set("123");
                //取數(shù)據(jù)
                System.out.printf("[%s]=[%s]\n",currentThread.getName(),threadLocal.get());
            }
        }).start();
        TimeUnit.SECONDS.sleep(1);
        System.out.println(threadLocal.get());
    }
}

通過使用ThreadLocal,它既能保證多線程訪問的安全性,同時也能實現(xiàn)無鎖。

實現(xiàn)原理

要想實現(xiàn)ThreadLocal這樣的效果我們基本山能想到的就兩種方式方式來實現(xiàn)婿奔。

ThreadLocal維護(hù)Thread與數(shù)據(jù)的映射關(guān)系

該實現(xiàn)方式是在ThreadLocal內(nèi)部維護(hù)一個線程安全的Map荒给,然后以當(dāng)前線程作為Map的Key胖烛,而線程的數(shù)據(jù)作為Value宽档。

ThreadLocal維護(hù)Thread與數(shù)據(jù)的映射關(guān)系.jpg

上面的這種方式能實現(xiàn)ThreadLocal的功能讲岁,但是問題在于通過這種方式實現(xiàn)就必須要保證ThreadLocal中負(fù)責(zé)維護(hù)線程和數(shù)據(jù)的Map線程安全主之,這或多或少都需要增加鎖的引入颂鸿,并不能實現(xiàn)無鎖促绵。

Thread維護(hù)ThreadLocal與數(shù)據(jù)的映射關(guān)系

通過上面的分析我們知道如果在ThreadLocal中維護(hù)Thread與數(shù)據(jù)的映射我們需要必須要保證內(nèi)部映射關(guān)系的線程安全,如果我們在Thread內(nèi)存維護(hù)一個ThreadLocal與數(shù)據(jù)之前的映射關(guān)系嘴纺,這種映射關(guān)系并沒有涉及到線程安全問題败晴,這樣也就省去了線程同步的操作,相比上面的實現(xiàn)方式栽渴,該方式性能上更好尖坤。而JDK內(nèi)部就是使用該方式來實現(xiàn)的。

Thread維護(hù)ThreadLocal與數(shù)據(jù)的映射關(guān)系.jpg

基本使用

實例化

示例化方式一般就兩種闲擦,第一種是直接使用無參構(gòu)造函數(shù)創(chuàng)建慢味,第二種則是在1.8的版本提供的靜態(tài)方法創(chuàng)建。

public class App8 {
    /**
     * 實例化方式1 構(gòu)造函數(shù)
     */
    public static ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
    /**
     * 實例化方式2 靜態(tài)方法,
     */
    public static ThreadLocal<String> threadLocal2 = ThreadLocal.withInitial(() -> {
        System.out.println("如果值為空時則會調(diào)用該方法獲取初始值");
        return "abc";
    });
    public static void main(String[] args) {
        System.out.println(threadLocal1.get());
        System.out.println(threadLocal2.get());
        threadLocal2.remove();
        System.out.println(threadLocal2.get());
    }
}

常用方法

常用的方法就三個墅冷,set()用來往ThreadLocal中設(shè)置值纯路,get()用來往ThreadLocal中獲取值,而remove()用來刪除ThreadLocal中的值寞忿。

public class App9 {
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    public static void main(String[] args) {
        //設(shè)置值
        threadLocal.set("abc");
        //獲取值
        String value = threadLocal.get();
        System.out.println(value);
        //刪除值
        threadLocal.remove();
        System.out.println(threadLocal.get());
    }
}

ThreadLocal中提供的方法比較簡單驰唬,但是在使用時需要特別注意remove方法。當(dāng)我們使用完ThreadLocal后我們應(yīng)該調(diào)用remove方法將ThreadLocal中的數(shù)據(jù)清除腔彰,如果不這么做容易產(chǎn)生業(yè)務(wù)數(shù)據(jù)異常和內(nèi)存泄漏(后面將說明為什么會導(dǎo)致內(nèi)存泄漏)叫编。

public class App10 {
    public static ThreadLocal<List<Integer>> threadLocal = ThreadLocal.withInitial(() -> new ArrayList<>());
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 10; i++) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    int value = new Random().nextInt(1000);
                    threadLocal.get().add(value);
                    System.out.println(threadLocal.get());
                }
            });
        }
        executor.shutdown();
    }
}

最后的打印結(jié)果如下:

[562]
[725]
[562, 434]
[725, 590]
[562, 434, 448]
[725, 590, 377]
[562, 434, 448, 712]
[725, 590, 377, 57]
[562, 434, 448, 712, 715]
[725, 590, 377, 57, 580]

因為我們是在線程池中使用ThreadLocal,而線程池中的線程并不是執(zhí)行完之后就銷毀了霹抛。而代碼中并沒有調(diào)用remove方法清除ThreadLocal中的值搓逾,這就導(dǎo)致了List中保留了上一次任務(wù)的執(zhí)行結(jié)果。

源碼分析

我們在實現(xiàn)原理中大致講過ThreadLocal是如何實現(xiàn)的杯拐,我們先看如何往ThreadLocal中存儲值的霞篡。

public void set(T value) {
    //獲取當(dāng)前的線程
    Thread t = Thread.currentThread();
    //獲取Map
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //往Map中放值,Map的key就是當(dāng)前的ThreadLocal實例
        map.set(this, value);
    else
        //如果Map為空則創(chuàng)建Map,并將值放入Map中
        createMap(t, value);
}

上面代碼中的getMap(t)方法的實現(xiàn)如下:

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

代碼很簡單藕施,返回的就是ThreadLocal中的字段threadLocals寇损,它的類型就是ThreadLocalMap凸郑。我們調(diào)用set方法就是往Map中存放存放值裳食,而這個Map的key就是ThreadLocal,它的值就是我們要存的值芙沥。

public T get() {
    //獲取當(dāng)前線程
    Thread t = Thread.currentThread();
    //獲取Map
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //獲取Entry中的值
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //如果Map為空則獲取withInitial中supplier返回的值
    return setInitialValue();
}

為何會內(nèi)存泄漏

通過上面我們知道了ThreadLocal的實現(xiàn)原理诲祸,但是為何說會內(nèi)存泄漏呢浊吏?我們先看ThreadLocalMap的結(jié)構(gòu)。

ThreadLocalMap內(nèi)部結(jié)構(gòu).png

上圖就是ThreadLocal的結(jié)構(gòu)了救氯,它內(nèi)部提供了增刪改等主要方法找田,而ThreadLocal與值被封裝成Entry對象存放在table數(shù)組中。

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

從Entry中的結(jié)構(gòu)可以知道着憨,每一個Entry對象都是一個對Key弱的引用(關(guān)于什么是弱引用可以參考該文)墩衙,當(dāng)沒有強引用指向ThreadLocal變量時,ThreadLocal可以被回收甲抖。真是通過這種方式保證了ThreadLocal在沒有引用時而Thread還沒有被銷毀時可以被回收漆改。但是上面的問題帶來了另一個問題,當(dāng)Key被回收之后Entry對象并沒有被回收而導(dǎo)致內(nèi)存泄漏准谚。

如何解決

對于ThreadLocal中存在的內(nèi)存泄漏問題挫剑,最簡單的解決方案就是我們在每次在使用完ThreadLocal后手動的調(diào)用remove清除數(shù)據(jù)。但是如果你沒有這么做柱衔,ThreadLocal對于存在的內(nèi)存泄漏問題也做了部分優(yōu)化樊破。在set和get方法中,都會間接或直接的調(diào)用cleanSomeSlots唆铐、expungeStaleEntry哲戚、replaceStaleEntry等方法將key為空的Entry清除掉。雖然對于內(nèi)存泄漏ThreadLocal內(nèi)部已經(jīng)做了優(yōu)化或链,但是我們在使用最好還是在ThreadLocal不再使用時手動調(diào)用remove方法清除掉其中的數(shù)據(jù)惫恼,從而避免內(nèi)存泄漏。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末澳盐,一起剝皮案震驚了整個濱河市祈纯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌叼耙,老刑警劉巖腕窥,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異筛婉,居然都是意外死亡簇爆,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進(jìn)店門爽撒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來入蛆,“玉大人,你說我怎么就攤上這事硕勿∩诨伲” “怎么了?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵源武,是天一觀的道長扼褪。 經(jīng)常有香客問我想幻,道長,這世上最難降的妖魔是什么话浇? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任脏毯,我火速辦了婚禮,結(jié)果婚禮上幔崖,老公的妹妹穿的比我還像新娘食店。我一直安慰自己,他們只是感情好赏寇,可當(dāng)我...
    茶點故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布叛买。 她就那樣靜靜地躺著,像睡著了一般蹋订。 火紅的嫁衣襯著肌膚如雪率挣。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天露戒,我揣著相機與錄音椒功,去河邊找鬼。 笑死智什,一個胖子當(dāng)著我的面吹牛动漾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播荠锭,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼旱眯,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了证九?” 一聲冷哼從身側(cè)響起删豺,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎愧怜,沒想到半個月后呀页,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡拥坛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年蓬蝶,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片猜惋。...
    茶點故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡丸氛,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出著摔,到底是詐尸還是另有隱情缓窜,我是刑警寧澤,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站雹洗,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏卧波。R本人自食惡果不足惜时肿,卻給世界環(huán)境...
    茶點故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望港粱。 院中可真熱鬧螃成,春花似錦、人聲如沸查坪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽偿曙。三九已至氮凝,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間望忆,已是汗流浹背罩阵。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留启摄,地道東北人稿壁。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像歉备,于是被迫代替她去往敵國和親傅是。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,514評論 2 348