Java基礎(chǔ)之ThreadLocal

ThreadLocal 是什么

首先 它是一個(gè)數(shù)據(jù)結(jié)構(gòu) 類似HashMap 可以保存 Key Value 鍵值對 但是ThreadLocal只能保存一個(gè) 并且每個(gè)線程互不干擾

public static void main(String[] args) {
       final ThreadLocal<String> localName = new ThreadLocal();
        final HashMap<Integer, String> map = new HashMap<>(2);
        new Thread("線程1") {
            @Override
            public void run() {
                localName.set("Sincerity");
                String s = localName.get();
                System.out.println(Thread.currentThread().getName() + "獲取到ThreadLocal值=" + s);
                map.put(0, Thread.currentThread().getName());
                System.out.println(Thread.currentThread().getName() + "獲取到map的長度" + map.size());
            }
        }.start();
        String s = localName.get();
        System.out.println("主線程獲取到ThreadLocal值=" + s);
        new Thread("線程2") {
            @Override
            public void run() {
                String s = localName.get();
                System.out.println(Thread.currentThread().getName() + "獲取到ThreadLocal值=" + s);
                System.out.println(Thread.currentThread().getName() + "獲取到map的長度" + map.size());
            }
        }.start();
 //得到結(jié)果
主線程獲取到ThreadLocal值=null
 
線程1獲取到ThreadLocal值=Sincerity
線程1獲取到map的長度1
    
線程2獲取到ThreadLocal值=null
線程2獲取到map的長度1

思考一下為什么會(huì)出現(xiàn)這樣的情況呢 我們已經(jīng)知道ThreadLocal是一種數(shù)據(jù)結(jié)構(gòu) 為什么除了賦值的線程之外數(shù)據(jù)無法獲取呢 同樣是HashMap 為什么可以可以全局獲取到數(shù)據(jù)呢 帶著問題 我們一起探索一下

為何ThreadLocal能實(shí)現(xiàn)每個(gè)線程的數(shù)據(jù)互不干擾

讀懂源碼
public class ThreadLocal<T> {  
    ...
        //說明創(chuàng)建ThreadLocal的時(shí)候什么也沒有做
        public ThreadLocal() {
    }
    ...
     //set方法怎么說 
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t); //默認(rèn)情況下為null
        if (map != null)
            //set的時(shí)候 把自己當(dāng)做Key 傳遞的值當(dāng)做Value
            map.set(this, value);
        else
            createMap(t, value); //創(chuàng)建一個(gè)map對象
    }
    ...

     //獲取線程中保留的 ThreadLocal的映射 默認(rèn)在Thread中為空
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }    
     //創(chuàng)建一個(gè)ThreadLocalMap 
     void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
    //get方法 
     public T get() {
        Thread t = Thread.currentThread();
         //得到當(dāng)前線程的ThreadLocalMap映射
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //拿到key等于當(dāng)前ThreadLocal的Entry 
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
         //處理map等于null的情況 
        return setInitialValue();
    }
    /**
     *主要就是將一個(gè)null重新存入map中 并且返回null 
     */
    private T setInitialValue() {
        T value = initialValue();//得到一個(gè)Null值 
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
     protected T initialValue() {
        return null;
    }
}

看到這里其實(shí)我們也就明白 ThreadLocal為什么能保證每個(gè)線程數(shù)據(jù)獨(dú)立了 其內(nèi)部維護(hù)著一個(gè)當(dāng)前線程的映射ThreadLocalMap 然后通過線程映射得到當(dāng)前線程的ThreadLocalMap 這里就出現(xiàn)了一個(gè)問題 同一個(gè)ThreadLocal的Hashcode是一致的 怎么保證每個(gè)線程的數(shù)據(jù)獨(dú)立呢

看看ThreadLocalMap

   static class ThreadLocalMap {
       //數(shù)組中的桶 弱引用
       static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        private static final int INITIAL_CAPACITY = 16;
        private Entry[] table;
       //得到key的hashCode
        private final int threadLocalHashCode = nextHashCode();
       //生成hash code間隙為這個(gè)魔數(shù)阶剑,
       //可以讓生成出來的值或者說ThreadLocal的ID較為均勻地分布在2的冪大小的數(shù)組中摔蓝。
        private static final int HASH_INCREMENT = 0x61c88647;
        private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
       //構(gòu)造方法 默認(rèn)添加一個(gè)值 
           ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
             //創(chuàng)建一個(gè)默認(rèn)大小為16的數(shù)組
            table = new Entry[INITIAL_CAPACITY];
             //用firstKey的threadLocalHashCode與初始大小16取模得到哈希值
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            //設(shè)置閾值 
            setThreshold(INITIAL_CAPACITY);
        }
         private void setThreshold(int len) {
            threshold = len * 2 / 3; //直接寫成2/3了 ....
        }
       //向ThreadLocalMap中添加元素
        private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            //得到key的hashCode  線性探測法得到 
            //每個(gè)ThreadLocal對象都有一個(gè)hash值threadLocalHashCode财剖,每初始化一個(gè)ThreadLocal對象奠货,
            //hash值就增加一個(gè)固定的大小0x61c88647
            int i = key.threadLocalHashCode & (len-1);
            //根據(jù)ThreadLocal大小的hash值得到table中的i的元素 
            for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
                //如果I位置已經(jīng)有一個(gè)Entry對象 說明hash沖突了
                //得到當(dāng)前存儲(chǔ)元素的key 
                ThreadLocal<?> k = e.get();
                //如果這個(gè)元素額key正好是設(shè)置的key 重新給元素中的value賦值
                if (k == key) {
                    e.value = value;
                    return;
                }
               // 當(dāng)前i位置entry對象為空
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //如果當(dāng)前key的hashCode位置為空 插入一個(gè)enrty在i位置 
            tab[i] = new Entry(key, value);
            int sz = ++size;
            //清理一個(gè)沒用的數(shù)據(jù) 后大小達(dá)到閾值
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                //擴(kuò)容
                rehash(); //2倍擴(kuò)容
        }
   }

int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

可以看出偶器,它是在上一個(gè)被構(gòu)造出的ThreadLocal的ID/threadLocalHashCode的基礎(chǔ)上加上一個(gè)魔數(shù)0x61c88647的搀矫。這個(gè)魔數(shù)的選取與斐波那契散列有關(guān)官觅,0x61c88647對應(yīng)的十進(jìn)制為1640531527刑峡。斐波那契散列的乘數(shù)可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769洋闽,如果把這個(gè)值給轉(zhuǎn)為帶符號(hào)的int玄柠,則會(huì)得到-1640531527。換句話說
(1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))得到的結(jié)果就是1640531527也就是0x61c88647诫舅。通過理論與實(shí)踐羽利,當(dāng)我們用0x61c88647作為魔數(shù)累加為每個(gè)ThreadLocal分配各自的ID也就是threadLocalHashCode再與2的冪取模,得到的結(jié)果分布很均勻刊懈。
ThreadLocalMap使用的是線性探測法这弧,均勻分布的好處在于很快就能探測到下一個(gè)臨近的可用slot,從而保證效率虚汛。這就回答了上文拋出的為什么大小要為2的冪的問題匾浪。為了優(yōu)化效率。

對于& (INITIAL_CAPACITY - 1)卷哩,相信有過算法競賽經(jīng)驗(yàn)或是閱讀源碼較多的程序員蛋辈,一看就明白,對于2的冪作為模數(shù)取模殉疼,可以用&(2n-1)來替代%2n梯浪,位運(yùn)算比取模效率高很多。至于為什么瓢娜,因?yàn)閷?^n取模,只要不是低n位對結(jié)果的貢獻(xiàn)顯然都是0句喷,會(huì)影響結(jié)果的只能是低n位悉稠。

可以說在ThreadLocalMap中喻频,形如key.threadLocalHashCode & (table.length - 1)(其中key為一個(gè)ThreadLocal實(shí)例)這樣的代碼片段實(shí)質(zhì)上就是在求一個(gè)ThreadLocal實(shí)例的哈希值,只是在源碼實(shí)現(xiàn)中沒有將其抽為一個(gè)公用函數(shù)褒颈。

內(nèi)存泄漏

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

通過之前的分析已經(jīng)知道,當(dāng)使用ThreadLocal保存一個(gè)value時(shí)励堡,會(huì)在ThreadLocalMap中的數(shù)組插入一個(gè)Entry對象谷丸,按理說key-value都應(yīng)該以強(qiáng)引用保存在Entry對象中,但在ThreadLocalMap的實(shí)現(xiàn)中应结,key被保存到了WeakReference對象中刨疼。

這就導(dǎo)致了一個(gè)問題,ThreadLocal在沒有外部強(qiáng)引用時(shí)鹅龄,發(fā)生GC時(shí)會(huì)被回收揩慕,如果創(chuàng)建ThreadLocal的線程一直持續(xù)運(yùn)行,那么這個(gè)Entry對象中的value就有可能一直得不到回收扮休,發(fā)生內(nèi)存泄露迎卤。

如何避免內(nèi)存泄露

既然已經(jīng)發(fā)現(xiàn)有內(nèi)存泄露的隱患,自然有應(yīng)對的策略玷坠,在調(diào)用ThreadLocal的get()蜗搔、set()可能會(huì)清除ThreadLocalMap中key為null的Entry對象劲藐,這樣對應(yīng)的value就沒有GC Roots可達(dá)了,下次GC的時(shí)候就可以被回收樟凄,當(dāng)然如果調(diào)用remove方法瘩燥,肯定會(huì)刪除對應(yīng)的Entry對象。

如果使用ThreadLocal的set方法之后不同,沒有顯示的調(diào)用remove方法厉膀,就有可能發(fā)生內(nèi)存泄露,所以養(yǎng)成良好的編程習(xí)慣十分重要二拐,使用完ThreadLocal之后服鹅,記得調(diào)用remove方法。

ThreadLocal<String> localName = new ThreadLocal();
try {
    localName.set("Sincerity");
} finally {
    localName.remove();
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末百新,一起剝皮案震驚了整個(gè)濱河市企软,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌饭望,老刑警劉巖仗哨,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異铅辞,居然都是意外死亡厌漂,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門斟珊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來苇倡,“玉大人,你說我怎么就攤上這事囤踩≈冀罚” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵堵漱,是天一觀的道長综慎。 經(jīng)常有香客問我,道長勤庐,這世上最難降的妖魔是什么示惊? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮埃元,結(jié)果婚禮上涝涤,老公的妹妹穿的比我還像新娘。我一直安慰自己岛杀,他們只是感情好阔拳,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般糊肠。 火紅的嫁衣襯著肌膚如雪辨宠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天货裹,我揣著相機(jī)與錄音嗤形,去河邊找鬼。 笑死弧圆,一個(gè)胖子當(dāng)著我的面吹牛赋兵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播搔预,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼霹期,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了拯田?” 一聲冷哼從身側(cè)響起历造,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎船庇,沒想到半個(gè)月后吭产,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡鸭轮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年臣淤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片张弛。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡荒典,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出吞鸭,到底是詐尸還是另有隱情,我是刑警寧澤覆糟,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布刻剥,位于F島的核電站,受9級特大地震影響滩字,放射性物質(zhì)發(fā)生泄漏造虏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一麦箍、第九天 我趴在偏房一處隱蔽的房頂上張望漓藕。 院中可真熱鬧,春花似錦挟裂、人聲如沸享钞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽栗竖。三九已至暑脆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間狐肢,已是汗流浹背添吗。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留份名,地道東北人碟联。 一個(gè)月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像僵腺,于是被迫代替她去往敵國和親鲤孵。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344