ThreadLocal簡單分析與實(shí)現(xiàn)原理

《Java并發(fā)編程之美》讀書筆記

ThreadLocal

多線程在訪問同一個(gè)共享變量的時(shí)候容易出現(xiàn)并發(fā)的問題惊豺,特別是在多個(gè)線程對(duì)同一個(gè)共享變量進(jìn)行寫入的時(shí)候敬锐,一般都要對(duì)共享變量進(jìn)行適當(dāng)?shù)耐健?br> 同步的措施一般都是加鎖袜啃,這就需要使用者對(duì)鎖有一定的了解芳绩,這顯然增加了使用者的負(fù)擔(dān)蛾绎,那么有沒有一種方式可以做到昆箕,當(dāng)創(chuàng)建一個(gè)變量后,每個(gè)線程對(duì)其進(jìn)行訪問的時(shí)候訪問的是自己線程的變量呢->ThreadLocal
ThreadLocal是JDK包提供的租冠,它提供了線程本地變量鹏倘,也就是如果你創(chuàng)建了一個(gè)ThreadLocal,那么訪問這個(gè)變量的每個(gè)線程都會(huì)有這和個(gè)變量本地的一個(gè)副本顽爹。當(dāng)多個(gè)線程操作這個(gè)ThreadLocal變量時(shí)纤泵,實(shí)際上是在操作自己本地內(nèi)存里面的變量,從而避免了線程安全問題镜粤。創(chuàng)建了一個(gè)ThreadLocal變量后捏题,每個(gè)線程都會(huì)復(fù)制一個(gè)變量復(fù)制到自己的本地內(nèi)存

ThreadLocal使用示例

public class ThreadLocalTest {
    static void print(String str){
    //獲取到當(dāng)前線程本地內(nèi)存中的localVariable值
        System.out.println(str+":"+localVariable.get());
        //刪除當(dāng)前線程本地內(nèi)存中的localVariable值
        localVariable.remove();
    }
    //創(chuàng)建ThreadLocal變量
    static ThreadLocal<String> localVariable=new ThreadLocal<>();

    public static void main(String[] args) {
        Thread threadOne=new Thread(new Runnable() {
            @Override
            public void run() {
    //設(shè)置當(dāng)前線程本地內(nèi)存中的localVariable值
                localVariable.set("threadOne local variable");
                print("threadOne");
                System.out.println("threadOne remove after"+":"+localVariable.get());
            }
        });
        Thread threadTwo=new Thread(new Runnable() {
            @Override
            public void run() {
                localVariable.set("threadTwo local variable");
                print("threadTwo");
                System.out.println("threadTwo remove after"+":"+localVariable.get());
            }
        });
        threadOne.start();
        threadTwo.start();
    }
}

本例子開啟了兩個(gè)線程,在每個(gè)線程內(nèi)部設(shè)置了本地變量的值肉渴,然后調(diào)用print函數(shù)打印當(dāng)前本地變量的值公荧,如果打印后調(diào)用了本地變量的remove方法之后,則會(huì)刪除本地內(nèi)存中的共享變量同规。


線程One run方法通過設(shè)置localvariable的值循狰,這其實(shí)是設(shè)置的是線程one本地內(nèi)存中的一個(gè)副本窟社,這個(gè)副本線程two是訪問不了的。

ThreadLocal實(shí)現(xiàn)原理

Thread類內(nèi)部會(huì)有一個(gè)threadLocals和inheritableThreadLocals,他們都是ThreadLocalMap類型的變量绪钥,而ThreadLocalMap是一個(gè)定制化的hashMap灿里,在默認(rèn)的情況下,每個(gè)線程中的兩個(gè)變量都為null昧识,只有當(dāng)前線程第一次調(diào)用ThreadLocal的set或者get方法之后才會(huì)創(chuàng)建他們钠四,其實(shí)每個(gè)線程的本地變量并不是存在ThreadLocal實(shí)例里面盗扒,而是存放在調(diào)用線程的threadLocals里面跪楞,也就是說ThreadLocal類型的本地變量存放在具體的線程的內(nèi)存空間中,ThreadLocal就是一個(gè)空殼侣灶,它通過set方法把value值放入線程的threadlocals里面存放起來甸祭,當(dāng)調(diào)用線程使用它的get方法時(shí),再從當(dāng)前線程的threadlocals變量里面將其拿出來褥影,如果調(diào)用線程一直不終止池户,那么這個(gè)本地變量會(huì)一直存放在調(diào)用線程的threadlocals里面,所以當(dāng)不需要使用本地變量的時(shí)候凡怎,可以通過調(diào)用ThreadLocal變量里面的remove方法校焦,從當(dāng)前線程的threadlocals里面刪除該本地變量
另外 Thread里面的threadlocals為什么設(shè)置為map結(jié)構(gòu)?很明顯是因?yàn)槎鄠€(gè)線程可以關(guān)聯(lián)多個(gè)ThreadLocal變量统倒。

簡單分析ThreadLocal的set寨典,get以及remove方法的是實(shí)現(xiàn)邏輯

  1. public void set(T value)
 public void set(T value) {
        //獲取到當(dāng)前線程
        Thread t = Thread.currentThread();
        //將當(dāng)前線程作為key去找對(duì)應(yīng)的線程變量,找到則設(shè)置
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
        //如果是第一次調(diào)用就創(chuàng)建當(dāng)前線程對(duì)應(yīng)的HashMap
            createMap(t, value);
        }
    }

代碼中首先獲取調(diào)用線程房匆,然后使用當(dāng)前線程作為參數(shù)調(diào)用getMap(t)方法


可以看到耸成,getMap(t)的作用是獲取線程自己的變量threadlocals,threadlocals變量被綁定到了線程的成員變量上。
如果getMap(t)返回值不為空浴鸿,則把value值設(shè)置到threadLocals中井氢,也就是把變量值放入到當(dāng)前線程的內(nèi)存變量threadLocals中。threadLocals是一個(gè)HashMap結(jié)構(gòu)岳链,其中的key就是當(dāng)前ThreadLocal的實(shí)例對(duì)象的引用花竞,value是通過set方法傳遞的值
如果getMap(t)返回空值則說明是第一次調(diào)用set方法,這時(shí)用 createMap(t, value)創(chuàng)建當(dāng)前線程的threadLocals變量掸哑。

createMap創(chuàng)建當(dāng)前線程的threadLocals變量约急。

  1. T get()
  public T get() {
        //獲取當(dāng)前線程
        Thread t = Thread.currentThread();
        //獲取當(dāng)前線程的threadLocals變量 是一個(gè)ThreadLocalMap結(jié)構(gòu)
        ThreadLocalMap map = getMap(t);
        //如果threadLocals不為空,則返回對(duì)應(yīng)本地變量的值
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //threadLocals為空的話就初始化當(dāng)前線程的threadLocals變量
        return setInitialValue();
    }

上訴代碼首先獲取當(dāng)前線程的實(shí)例举户,在獲取當(dāng)前線程的threadLocals變量烤宙,如果不為null則直接返回當(dāng)前線程綁定的本地變量,否則執(zhí)行代碼初始化俭嘁。

private T setInitialValue() {
        //初始化為null
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        //如果當(dāng)前線程的threadLocals變量不為空
        if (map != null) {
        //value為null
            map.set(this, value);
        } else {
          //如果當(dāng)前線程的threadLocals變量為空
            createMap(t, value);
        }
        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        return value;
    }

如果當(dāng)前線程的threadLocals變量不為空躺枕,則設(shè)置為當(dāng)前線程的本地變量值為null,否則調(diào)用createMap(t, value)方法創(chuàng)建當(dāng)前線程的threadLocals變量。

  1. void remove()


如果當(dāng)前線程的threadLocals變量不為空拐云,則刪除當(dāng)前線程中指定ThreadLocal實(shí)例即this的本地變量罢猪。
總結(jié)
在每個(gè)線程內(nèi)部都有一個(gè)名為threadLocals的成員變量,這個(gè)變量的類型是HashMap叉瘩,其中key就為我們定義的ThreadLocal類型的變量的this引用膳帕,value則為我們用set方法設(shè)置的值,每個(gè)線程的本地變量存放在線程自己的內(nèi)存變量threadLocals里面薇缅,如果當(dāng)前線程一直不消亡危彩,那么這些本地變量就會(huì)一直存在,所以可能會(huì)造成內(nèi)存溢出泳桦,因此使用完畢后記得調(diào)用ThreadLocal的remove方法刪除對(duì)應(yīng)線程的threadLocals中的本地變量汤徽。
注:在JUC包里面的ThreadLocalRandom,就是借鑒這中思想實(shí)現(xiàn)的灸撰。

ThreadLocal不支持繼承性

public class ThreadLocalDemo {
    //創(chuàng)建線程變量
    private static ThreadLocal<String> threadLocal=new ThreadLocal<>();
    public static void main(String[] args) {
        threadLocal.set("helloworld");
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread:"+threadLocal.get());
            }
        });
        thread.start();
        System.out.println("main:"+threadLocal.get());
    }
}

也就是說谒府,同一個(gè)ThreadLocal變量在父線程中設(shè)置值后,在子線程中是獲取不到的浮毯,如之前所說完疫,這是很正常的現(xiàn)象,因?yàn)樽泳€程thread里面調(diào)用get方法時(shí)當(dāng)前線程為thread線程债蓝,而這里調(diào)用set方法設(shè)置線程變量的是main線程壳鹤,兩者是不同的線程所以子線程訪問時(shí)為null;

Inheritable ThreadLocal類

為了解決ThreadLocal類不支持繼承性的這個(gè)問題惦蚊,InheritableThreadLocal應(yīng)運(yùn)而生器虾,它繼承自ThreadLocal,提供了一個(gè)屬性蹦锋,就是讓子線程可以訪問可以訪問在父線程中設(shè)置的本地變量兆沙,
InheritableThreadLocal源代碼:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
  
    protected T childValue(T parentValue) {
        return parentValue;
    }

    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

由以上代碼可知,InheritableThreadLocal繼承ThreadLocal類莉掂,并且重寫了三個(gè)方法葛圃。重寫了createMap方法,所以現(xiàn)在第一次調(diào)用set方法的時(shí)候憎妙,創(chuàng)建的是當(dāng)前線程的t.inheritableThreadLocals變量的實(shí)例而不再是threadLocals實(shí)例再沧。當(dāng)調(diào)用get方法獲取當(dāng)前線程內(nèi)部的map變量時(shí)杨拐,獲取的是t.inheritableThreadLocals而不再是threadLocals裂七。
綜上鹃操,在InheritableThreadLocal世界里,變量由inheritableThreadLocals代替了threadLocals

觀察如何讓子線程可以訪問父線程的本地變量抚垃。

  public Thread(Runnable target) {
        this(null, target, "Thread-" + nextThreadNum(), 0);
    }
private Thread(ThreadGroup g, Runnable target, String name,
                   long stackSize, AccessControlContext acc,
                   boolean inheritThreadLocals) {
        //獲取當(dāng)前線程
        Thread parent = currentThread();
        //如果父線程的inheritThreadLocals不為null
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        //設(shè)置子線程中的inheritThreadLocals變量
            this.inheritableThreadLocals =
           ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        this.stackSize = stackSize;
        this.tid = nextThreadID();
    }

如上的代碼在創(chuàng)建的過程中喷楣,在構(gòu)造函數(shù)中會(huì)調(diào)用私有的構(gòu)造方法趟大,先獲取當(dāng)前的線程,這里是main函數(shù)所在的線程铣焊,也就是main線程逊朽,然后再判斷main函數(shù)所在的線程里面inheritableThreadLocals是否為空,然后就會(huì)執(zhí)行createInheritedMap方法


可以看到createInheritedMap的內(nèi)部使用的是父線程的inheritableThreadLocals變量作為構(gòu)造函數(shù)創(chuàng)建了一個(gè)新的ThreadLocalMap變量曲伊。

private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (Entry e : parentTable) {
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

在該構(gòu)造函數(shù)的內(nèi)部將父線程的inheritableThreadLocals成員變量復(fù)制到新的ThreadLocalMap變量當(dāng)中
總結(jié)叽讳,InheritableThreadLocal通過重寫ThreadLocal類的代碼讓本地變量保存到了具體的inheritableThreadLocals里面,那么線程在通過InheritableThreadLocal實(shí)例的set或者get方法設(shè)置變量時(shí)坟募,就會(huì)創(chuàng)建當(dāng)前線程的inheritableThreadLocals變量岛蚤。當(dāng)父線程創(chuàng)建子線程時(shí),構(gòu)造函數(shù)會(huì)把父線程中的inheritableThreadLocals變量里面的本地變量復(fù)制一份保存到子線程的inheritableThreadLocals里面
把之前的代碼改為:

 private static ThreadLocal<String> threadLocal=new InheritableThreadLocal<>();

可見婿屹,現(xiàn)在可以從子線程正常獲取到線程變量的值了灭美。
在什么情況下需要子線程可以獲取到父線程的thredLocal變量呢推溃?
比如子線程需要使用存放在threadLocal變量中的用戶信息昂利,再比如說一些中間件需要把統(tǒng)一的id追蹤的整個(gè)調(diào)用鏈路記錄下來。其實(shí)子線程使用父線程中的threadLocal方法有很多種铁坎,比如創(chuàng)建線程時(shí)候蜂奸,使用父線程中的變量,并將其復(fù)制到子線程中硬萍,或者在父線程中構(gòu)造一個(gè)map作為參數(shù)傳遞給子線程扩所。但是這些方法都改變了我們的使用習(xí)慣,所以在這些情況下InheritableThreadLocal就顯得比較有用朴乖。

參考資料:
《Java并發(fā)編程之美》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末祖屏,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子买羞,更是在濱河造成了極大的恐慌袁勺,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,718評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件畜普,死亡現(xiàn)場(chǎng)離奇詭異期丰,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)吃挑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門钝荡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人舶衬,你說我怎么就攤上這事埠通。” “怎么了逛犹?”我有些...
    開封第一講書人閱讀 158,207評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵端辱,是天一觀的道長蟹瘾。 經(jīng)常有香客問我,道長掠手,這世上最難降的妖魔是什么憾朴? 我笑而不...
    開封第一講書人閱讀 56,755評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮喷鸽,結(jié)果婚禮上众雷,老公的妹妹穿的比我還像新娘。我一直安慰自己做祝,他們只是感情好砾省,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,862評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著混槐,像睡著了一般编兄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上声登,一...
    開封第一講書人閱讀 50,050評(píng)論 1 291
  • 那天狠鸳,我揣著相機(jī)與錄音,去河邊找鬼悯嗓。 笑死件舵,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的脯厨。 我是一名探鬼主播铅祸,決...
    沈念sama閱讀 39,136評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼合武!你這毒婦竟也來了临梗?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,882評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤稼跳,失蹤者是張志新(化名)和其女友劉穎盟庞,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體岂贩,經(jīng)...
    沈念sama閱讀 44,330評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡茫经,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,651評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了萎津。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片卸伞。...
    茶點(diǎn)故事閱讀 38,789評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖锉屈,靈堂內(nèi)的尸體忽然破棺而出荤傲,到底是詐尸還是另有隱情,我是刑警寧澤颈渊,帶...
    沈念sama閱讀 34,477評(píng)論 4 333
  • 正文 年R本政府宣布遂黍,位于F島的核電站终佛,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏雾家。R本人自食惡果不足惜铃彰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,135評(píng)論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望芯咧。 院中可真熱鬧牙捉,春花似錦、人聲如沸敬飒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽无拗。三九已至带到,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間英染,已是汗流浹背揽惹。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評(píng)論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留税迷,地道東北人永丝。 一個(gè)月前我還...
    沈念sama閱讀 46,598評(píng)論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像箭养,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子哥牍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評(píng)論 2 351

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

  • 2.1.11 ThreadLocal 多線程訪問同一個(gè)共享變量特別容易出現(xiàn)并發(fā)問題毕泌,特別是多個(gè)線程需要對(duì)一個(gè)共享變...
    阿里加多閱讀 389評(píng)論 0 1
  • 中間件興趣圈 , 作者 丁威 說起本地線程變量嗅辣,我相信大家首先會(huì)想到的是JDK默認(rèn)提供的ThreadLocal撼泛,用...
    xiaotian是個(gè)混子閱讀 838評(píng)論 1 1
  • 前言 要了解ThreadLocal,首先搞清楚ThreadLocal 是什么澡谭?是用來解決什么問題的愿题?ThreadL...
    薩達(dá)哈魯醬閱讀 475評(píng)論 0 7
  • 原理 產(chǎn)生線程安全問題的根源在于多線程之間的數(shù)據(jù)共享。如果沒有數(shù)據(jù)共享蛙奖,就沒有多線程并發(fā)安全問題潘酗。ThreadLo...
    Java耕耘者閱讀 298評(píng)論 0 0
  • 蘋果macOS操作系統(tǒng)下,隱藏文件是否顯示有很多種設(shè)置方法雁仲,最簡單的要算在Mac終端輸入命令仔夺,然后重新啟動(dòng)Find...
    憶昔溪閱讀 1,535評(píng)論 0 1