15. 吃了個包子,我突然明白ThreadLocal原理了?!

前言

共享變量一直是并發(fā)中比較讓人頭疼的問題,

每個線程都對它有操作權(quán), 所以線程之間的同步就顯得很關(guān)鍵.

前幾章說了很多, 大部分解決之道都和“鎖”相關(guān)!

總兒言之就是對于“共享變量”進行“鎖”控制, 確保某一時刻只有一個線程拿著這個變量, 來解決共享變量的爭用問題.

可是大家也知道, 加“鎖”這個還是比較消耗性能的, 有沒有更好的方式呢?

于是, 大佬們開始思考了:

其實沒必要對所有場景進行加鎖操作, 對某些場景采用生成“副本”的方式來解決顯得更加合適且高效!

經(jīng)過討論 , 總結(jié), 大佬們將共享變量分為以下兩種使用場景

  • 需要線程間同步

  • 無需線程間同步

為了說明這兩種場景, 咱舉個不太恰當(dāng)?shù)呛美斫獾睦?

假設(shè)10個人(線程)在包子鋪需要不多不少共吃完100個包子(共享變量), 你如何安排?

  • 方式一(需要線程間同步)
    10個人, 同時開吃, 每個人吃之前還得與其他人進行溝通, 及時了解還剩多少個包子可以吃(防止多吃或少吃)

  • 方式二(無需線程間同步)
    10個人, 開吃之前明確的被告知只能吃10個, 此時, 只需知道自己的10個有沒有吃完, 無需關(guān)心其他人吃了多少.

方式一就是我們平時加鎖的方式, 而方式二就是我們今天要學(xué)習(xí)ThreadLocal!

ThreadLocal介紹

通過以上的吃包子案例不難理解ThreadLocal的核心理念: 共享變量私有化.

每個線程都擁有一份共享變量的本地副本, 每個線程對應(yīng)一個副本, 同時對共享變量的操作也改為對屬于自己的副本的操作, 這樣每個線程處理自己的本地變量, 形成數(shù)據(jù)隔離.

ps: 每個人只關(guān)心自己有沒有吃完10個包子, 而不關(guān)心其他人吃了多少個!

ThreadLocal和Synchronized都是為了解決多線程中共享變量的訪問沖突問題

不同的是:

  • Synchronized通過線程等待, 犧牲時間來解決訪問沖突

  • ThreadLocal通過每個線程單獨一份存儲空間來存儲共享變量的副本, 犧牲空間來解決沖突

相比于Synchronized, ThreadLocal具有線程隔離的效果:

只有在線程內(nèi)才能獲取到對應(yīng)的值, 線程外則不能訪問到想要的值.

ThreadLocal最適合的是共享變量在線程間隔離, 而在本線程內(nèi)方法或類間共享的場景.

ThreadLocal使用

為了說明ThreadLocal的作用, 我們舉個例子

public class UserLocal {
    private static ThreadLocal<String> local = new ThreadLocal<String>() {
        //初始化個值
        @Override
        protected String initialValue() {
            return "init";
        }
    };
    //private static String threadName;
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                public void run() {
                    //threadlocal設(shè)置個線程隔離的值
                    local.set("線程值:" + Thread.currentThread().getName());
                    //普通共享變量
                    //threadName = Thread.currentThread().getName();
                    //睡眠一下,讓其它線程可以讀取本線程的local值
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "   " + local.get());
                    //System.out.println(Thread.currentThread().getName() + "   " + threadName);
                }
            }).start();
        }
    }
}

很簡單的案例, 聲明一個static ThreadLocal變量 local, 用于存放線程名稱.

為了說明ThreadLocal的線程隔離特性, 所以Thread.sleep(1)下, (理論上)讓其它線程可以讀取到當(dāng)前的local值.

按常規(guī)來說, ThreadLocal定義成了全局變量, 值應(yīng)該是共享的, 任何一個線程更改之后,其他線程應(yīng)該也會更改.

但從結(jié)果上來看, 線程間互不影響, 這就是ThreadLocal的線程隔離特性.

Thread-0   線程值:Thread-0
Thread-1   線程值:Thread-1
Thread-2   線程值:Thread-2
Thread-3   線程值:Thread-3
Thread-4   線程值:Thread-4
Thread-5   線程值:Thread-5
Thread-7   線程值:Thread-7
Thread-8   線程值:Thread-8
Thread-9   線程值:Thread-9
Thread-6   線程值:Thread-6

注: 案例中也寫了普通的共享變量用來對比, 大家自行嘗試!

ThreadLocal原理

直接打開ThreadLocal類文件, 查看代碼, 不難找到以下幾個函數(shù)

  • set()


    set()
  • get()


    get()
  • getMap()


    getMap()
  • createMap()


    createMap()
  • setInitialValue()


    setInitialValue()
  • remove()


    remove()

需要注意的就是getMap()這個方法, 它接受一個Thread對象作為參數(shù), 然后直接返回了這個Thread對象中的threadLocals成員變量.

ThreadLocal的set()吃靠、get()谈截、remove()等方法都是基于Thread對象中的threadLocals成員變量來進行工作的.

打開Thread類文件,發(fā)現(xiàn), Thread中的確定義了這么個成員變量,并且類型為ThreadLocal.ThreadLocalMap.

image.png

不妨再看看ThreadLocalMap類, IDEA直接command單擊查看類文件, 居然直接跳到了ThreadLocal類文件

ThreadLocalMap.class

原來, ThreadLocalMap是ThreadLocal的一個靜態(tài)內(nèi)部類!

不難看出, ThreadLocalMap就是個特殊點的Map結(jié)構(gòu), 也就是K-V結(jié)構(gòu), 只不過, 這個Key特殊點, 居然是個ThreadLocal類型的弱引用.

看到此處, 我們理清了Thread告喊、ThreadLocal、ThreadLocalMap的關(guān)系:

  • ThreadLocal雖然提供了set()、get()等存取方法, 但是它并不直接存儲數(shù)據(jù), 它只是作為一個Key來關(guān)聯(lián)Value, 并且這個Key是個弱引用

  • ThreadLocalMap是ThreadLocal的靜態(tài)內(nèi)部類, Thread實例會初始化ThreadLocalMap(也就是threadLocals成員變量), 并用它來真正保存K-V數(shù)據(jù)

為了加深理解, 我們再舉個例子

public class UserLocal {
    private static ThreadLocal<String> local1 = new ThreadLocal<String>();
    private static ThreadLocal<String> local2 = new ThreadLocal<String>();
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                public void run() {
                    local1.set("local1:" + Thread.currentThread().getName());
                    local2.set("local2:" + Thread.currentThread().getName());
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "   " + local1.get()+ "   " +local2.get());
                }
            }).start();
        }
    }
}    
  1. 首先實例了兩個ThreadLocal對象local1,local2;
  2. 線程實例執(zhí)行到local1.set()時, 會先獲取當(dāng)前線程實例的threadLocals(ThreadLocalMap類型)
  3. 此時當(dāng)前線程實例的threadLocals為null, 則初始化線程實例的threadLocals
  4. 將當(dāng)前ThreadLocal實例local1的弱引用作為key, "local1:" + Thread.currentThread().getName()作為value, 放入threadLocals中
  5. 線程實例執(zhí)行到local2.set()時, 會先獲取當(dāng)前線程實例的threadLocals
  6. 此時當(dāng)前線程實例的threadLocals不為null, 則直接返回
  7. 將當(dāng)前ThreadLocal實例local2的弱引用作為key, "local2:" + Thread.currentThread().getName()作為value, 放入threadLocals中
  8. ……

看到這, 想必一切都很清楚了.

不過可能有朋友和小白的咸魚君一樣, 對文章中提到的“弱引用”產(chǎn)生了疑惑.

也就是這段代碼


image.png

關(guān)于“弱引用”, 其實是Java中“引用類型”中的一種, 要是你和咸魚一樣好奇&小白, 不妨繼續(xù)往下看.

Java中的引用

8種基本類型

我們知道, Java中有8種基本類型


image

除了這8種基本類型外, 還有個引用類型的概念.

引用類型

那什么叫引用類型呢?

簡單來說

所有的非基本數(shù)據(jù)類型都是引用數(shù)據(jù)類型.

具體點就是:

  • 接口類型
  • 數(shù)組類型
  • 枚舉類型
  • 注解類型
  • 字符串型
  • 其它……

基本類型和引用類型的區(qū)別

區(qū)別其實很簡單: 基本數(shù)據(jù)類型是分配在棧上的, 而引用類型是分配在堆上的(需要java中的棧、堆概念)

舉個例子:

int a = 1;
String str = new String("abc");

我們創(chuàng)建了兩個變量, 他們都會先在棧中分配一塊內(nèi)存;

對于基本類型a來說, 這塊區(qū)域包含的是基本類型的內(nèi)容, 也就是1;
對于引用類型str來說坝冕,這塊區(qū)域包含的是指向真正內(nèi)容的指針(地址),真正的內(nèi)容“abc”被保存在堆內(nèi)存上.

這個差別也體現(xiàn)在我們常用的比對上, 比如“==”和“equals”的區(qū)別, 這里就不過多介紹了.

引用類型的類別

在 JDK.1.2 之后, Java 對引用的概念進行了擴充, 將引用按強度分為了以下4種

  • 強引用
  • 軟引用
  • 弱引用
  • 虛引用

引用就引用, 為什么還要區(qū)分出4種類型??

Java中區(qū)分這4種引用類型主要有兩個目的 :

  1. 可以讓程序員通過代碼的方式?jīng)Q定某些對象的生命周期
  2. 有利于JVM進行垃圾回收坐梯。

那什么是強引用徽诲、軟引用、弱引用以及虛引用呢?
我們舉例說明

強引用(Java中默認聲明的就是強引用)

把一個對象賦給一個引用變量,這個引用變量就是一個強引用;
當(dāng)一個對象被強引用變量引用時吵血,它處于可達狀態(tài)谎替,它是不可能被垃圾回收機制回收的,即使該對象以后永遠都不會被用到JVM也不會回收蹋辅。因此強引用是造成Java內(nèi)存泄漏的主要原因之一钱贯。只要強引用存在, 垃圾回收器將永遠不會回收被引用的對象;
哪怕內(nèi)存不足時, JVM也會直接拋出OutOfMemoryError, 不會去回收;
如果想中斷強引用與對象之間的聯(lián)系, 可以顯示的將強引用賦值為null, 這樣一來,JVM就可以適時的回收對象了

//new Object()實例化一個對象, 并將引用obj1指向這個對象
Object obj1 = new Object();
//obj2 將和 obj1 指向同一個對象
Object obj2= obj1;
//obj1 斷開了引用, 但是obj1指向的對象不會成為垃圾空間被gc回收, 因為 obj2還引用著這個對象
obj1 = null;
System.gc();
System.out.println("obj1:"+obj1);
System.out.println("obj2:"+obj2);
image.png

軟引用

軟引用是用來描述一些非必需但仍有用的對象.

內(nèi)存足夠時, 軟引用對象不會被回收;
只有在內(nèi)存不足時,系統(tǒng)則會回收軟引用對象;
如果回收了軟引用對象之后仍然沒有足夠的內(nèi)存, 才會拋出內(nèi)存溢出異常.
這種特性常常被用來實現(xiàn)緩存技術(shù),比如網(wǎng)頁緩存、圖片緩存等.

Object obj = new Object();
SoftReference softRef = new SoftReference(obj);
obj = null;
System.gc();
System.out.println("obj:"+obj);
System.out.println("softRef:"+softRef.get());
image.png

弱引用

弱引用的引用強度比軟引用要更弱一些;
無論內(nèi)存是否足夠, 只要 JVM 開始進行GC垃圾回收,那些被弱引用關(guān)聯(lián)的對象都會被回收

Object obj = new Object();
WeakReference weakRef = new WeakReference(obj);
obj = null;
System.gc();
System.out.println("obj:"+obj);
System.out.println("weakRef:"+weakRef.get());
image.png

虛引用

虛引用是最弱的一種引用關(guān)系;
虛引用與其它幾種引用都不同, 虛引用并不會決定對象的生命周期.
如果一個對象僅持有虛引用, 那么它就和沒有任何引用一樣,它隨時可能會被回收.
虛引用主要用來跟蹤對象被垃圾回收器回收的活動, 必須和引用隊列(ReferenceQueue)結(jié)合使用.
垃圾回收器回收對象時,該對象還有虛引用,就會在回收對象的內(nèi)存之前,把這個虛引用加入到與之關(guān)聯(lián)的引用隊列中(程序可以通過判斷引用隊列中是否已經(jīng)加入了引用,來判斷被引用的對象是否將要被垃圾回收,這樣就可以在對象被回收之前采取一些必要的措施.)

ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomReference = new PhantomReference<>(new Object(), queue);
System.out.println("phantomReference:"+phantomReference.get());
image.png

一張圖的說明ThreadLocal

先來張圖總結(jié)下上面對ThreadLocal的介紹

image.png

Thread運行時,線程的的一些局部變量和引用使用的內(nèi)存屬于Stack(棧)區(qū)侦另,而普通的對象是存儲在Heap(堆)區(qū).

  1. 線程運行時,我們定義的TheadLocal對象被初始化,存儲在Heap,同時線程運行的棧區(qū)保存了指向該實例的引用,也就是圖中的ThreadLocalRef

  2. 當(dāng)ThreadLocal的set/get被調(diào)用時,虛擬機會根據(jù)當(dāng)前線程的引用也就是CurrentThreadRef找到其對應(yīng)在堆區(qū)的實例,然后查看其對用的TheadLocalMap實例是否被創(chuàng)建,如果沒有,則創(chuàng)建并初始化

  3. Map實例化之后,也就拿到了該ThreadLocalMap的句柄,然后如果將當(dāng)前ThreadLocal對象作為key,進行存取操作

  4. 圖中的虛線,表示key對ThreadLocal實例的引用是個弱引用

老生常談的“內(nèi)存泄漏”問題

ThreadLocalMap是以弱引用的方式引用著ThreadLocal, 換句話說,就是ThreadLocal是被ThreadLocalMap以弱引用的方式關(guān)聯(lián)著.

如果ThreadLocal沒有被ThreadLocalMap以外的對象引用, 那么依據(jù)弱引用的特性, 在下一次GC的時候, ThreadLocal實例就會被回收.

此時ThreadLocalMap里的一組 K-V 的 K 就是null了, 那么此處的V便不會被外部訪問到.

并且只要Thread實例一直存在, Thread實例就強引用著ThreadLocalMap, 因此ThreadLocalMap就不會被回收, 那么這里K為null的V就一直占用著內(nèi)存.

TheadLocal本身的優(yōu)化

針對上面的弱引用引起的內(nèi)存泄漏問題, TheadLocal本身做了如下優(yōu)化

image.png

可以看到, 調(diào)用set(), Key為null時, 會執(zhí)行replaceStaleEntry(key, value, i)方法, 用當(dāng)前的值替換掉以前的Key為null的值, 重復(fù)利用了空間.

另外ThreadLocalMap的set()秩命、get()尉共、remove()方法還有有以下邏輯(這里以remove()為例):

當(dāng)Key為null時, 在下一次ThreadLocalMap調(diào)用remove()方法的時候會被清除value值.

image.png

image.png

為什么使用弱引用

既然ThreadLocalMap K-V 的 K 使用弱應(yīng)用關(guān)聯(lián)ThreadLocal會有內(nèi)存泄漏問題, 那為什么不使用強引用呢??

我們不如換個問法:

key 使用強引用會怎么樣?

若ThreadLocalMap的Key為強引用, 那么回收ThreadLocal時, 因為ThreadLocalMap還持有ThreadLocal的強引用, 如果沒有手動刪除, ThreadLocal不會被回收, 這會導(dǎo)致Entry內(nèi)存泄漏.

key 使用弱引用會怎么樣?

若ThreadLocalMap的key為弱引用, 那么回收ThreadLocal時,由于ThreadLocalMap持有ThreadLocal的弱引用, 即使沒有手動刪除, ThreadLocal也會被回收;
當(dāng)Key為null, 在下一次ThreadLocalMap調(diào)用set(), get(), remove()方法的時候會被清除value值.

總結(jié)

Thread包含ThreadLocalMap, 因此ThreadLocalMap與Thread的生命周期是一樣的.

如果沒有手動刪除對應(yīng)Key, 都會導(dǎo)致內(nèi)存泄漏.

不過, 使用弱引用可以多一層保障:

弱引用ThreadLocal不會使得Entry內(nèi)存泄漏; Key 為 null 時, 對應(yīng)的value在下一次ThreadLocalMap調(diào)用set(),get(),remove()的時候會被清除.

因此, ThreadLocal內(nèi)存泄漏的根源是:

ThreadLocalMap的生命周期跟Thread一樣長, 如果沒有手動刪除對應(yīng)Key就會導(dǎo)致內(nèi)存泄漏; 而不是因為弱引用.

ThreadLocal正確的使用方法

  1. 每次使用完ThreadLocal都調(diào)用它的remove()方法清除數(shù)據(jù)

  2. 將ThreadLocal變量定義成private static, 這樣就一直存在ThreadLocal的強引用, 也就能保證任何時候都能通過ThreadLocal的弱引用訪問到Entry的value值, 進而清除掉.

小結(jié)下

原本以為ThreadLocal這么簡單, 簡簡單單就寫完這篇了.

沒想到, 深扒一下源碼, 洋洋灑灑寫了近4-5千字, 希望能幫到大家的學(xué)習(xí)!

歡迎關(guān)注我

技術(shù)公眾號 “CTO技術(shù)”


訂閱號.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末弃锐,一起剝皮案震驚了整個濱河市袄友,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌霹菊,老刑警劉巖剧蚣,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異旋廷,居然都是意外死亡鸠按,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進店門饶碘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來目尖,“玉大人,你說我怎么就攤上這事扎运∩” “怎么了?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵绪囱,是天一觀的道長测蹲。 經(jīng)常有香客問我,道長鬼吵,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任篮赢,我火速辦了婚禮齿椅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘启泣。我一直安慰自己涣脚,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布寥茫。 她就那樣靜靜地躺著遣蚀,像睡著了一般。 火紅的嫁衣襯著肌膚如雪纱耻。 梳的紋絲不亂的頭發(fā)上芭梯,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天,我揣著相機與錄音弄喘,去河邊找鬼玖喘。 笑死,一個胖子當(dāng)著我的面吹牛蘑志,可吹牛的內(nèi)容都是我干的累奈。 我是一名探鬼主播贬派,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼澎媒!你這毒婦竟也來了搞乏?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤戒努,失蹤者是張志新(化名)和其女友劉穎请敦,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體柏卤,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡冬三,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了缘缚。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片勾笆。...
    茶點故事閱讀 40,567評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖桥滨,靈堂內(nèi)的尸體忽然破棺而出窝爪,到底是詐尸還是另有隱情,我是刑警寧澤齐媒,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布蒲每,位于F島的核電站,受9級特大地震影響喻括,放射性物質(zhì)發(fā)生泄漏邀杏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一唬血、第九天 我趴在偏房一處隱蔽的房頂上張望望蜡。 院中可真熱鬧,春花似錦拷恨、人聲如沸脖律。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽小泉。三九已至,卻和暖如春冕杠,著一層夾襖步出監(jiān)牢的瞬間微姊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工拌汇, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留柒桑,地道東北人。 一個月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓噪舀,卻偏偏與公主長得像魁淳,于是被迫代替她去往敵國和親飘诗。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,585評論 2 359

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

  • 什么是ThreadLocal ThreadLocal界逛,簡單翻譯過來就是本地線程昆稿,但是直接這么翻譯很難理解Threa...
    跟著Mic學(xué)架構(gòu)閱讀 1,789評論 2 4
  • ThreadLocal作用 對于Android程序員來說,很多人都是在學(xué)習(xí)消息機制時候了解到ThreadLocal...
    三雒閱讀 1,321評論 0 19
  • 1息拜、前言 我得為我之前的行為道歉溉潭,之前都是看別人的博客抄抄抄,結(jié)果抄了一波非重點的東西少欺,也不知道啥意思喳瓣。其實不管是...
    放開那個BUG閱讀 365評論 0 0
  • 最近學(xué)習(xí) ThreadLocal,查看了很多資料赞别,用自己的思考方式再分析一遍 ThreadLocal 的內(nèi)存泄露問...
    Murray66閱讀 2,898評論 2 5
  • 久違的晴天畏陕,家長會。 家長大會開好到教室時仿滔,離放學(xué)已經(jīng)沒多少時間了惠毁。班主任說已經(jīng)安排了三個家長分享經(jīng)驗。 放學(xué)鈴聲...
    飄雪兒5閱讀 7,525評論 16 22