(八)ThreadLocal的使用及原理分析

什么是ThreadLocal

ThreadLocal拣技,簡(jiǎn)單翻譯過(guò)來(lái)就是本地線程脉幢,但是直接這么翻譯很難理解ThreadLocal的作用遭商,如果換一種說(shuō)法琼富,可以稱(chēng)為線程本地存儲(chǔ)盘榨。簡(jiǎn)單來(lái)說(shuō)娜搂,就是ThreadLocal為共享變量在每個(gè)線程中都創(chuàng)建一個(gè)副本漠秋,每個(gè)線程可以訪問(wèn)自己內(nèi)部的副本變量懈息。這樣做的好處是可以保證共享變量在多線程環(huán)境下訪問(wèn)的線程安全性

ThreadLocal的使用

沒(méi)有使用ThreadLocal時(shí)

通過(guò)一個(gè)簡(jiǎn)單的例子來(lái)演示一下ThreadLocal的作用风范,這段代碼是定義了一個(gè)靜態(tài)的成員變量num咨跌,然后通過(guò)構(gòu)造5個(gè)線程對(duì)這個(gè)num做遞增

public class ThreadLocalDemo {

    private static Integer num=0;

    public static void main(String[] args) {
        Thread[] threads=new Thread[5];
        for(int i=0;i<5;i++){
            threads[i]=new Thread(()->{
               num+=5;
               System.out.println(Thread.currentThread().getName()+" : "+num);
            },"Thread-"+i);
        }

        for(Thread thread:threads){
            thread.start();
        }
    }
}

運(yùn)行結(jié)果

Thread-0 : 5
Thread-1 : 10
Thread-2 : 15
Thread-3 : 20
Thread-4 : 25

每個(gè)線程都會(huì)對(duì)這個(gè)成員變量做遞增,如果線程的執(zhí)行順序不確定硼婿,那么意味著每個(gè)線程獲得的結(jié)果也是不一樣的锌半。

使用了ThreadLocal以后

通過(guò)ThreadLocal對(duì)上面的代碼做一個(gè)改動(dòng)

public class ThreadLocalDemo {

    private static final ThreadLocal<Integer> local=new ThreadLocal<Integer>(){
        protected Integer initialValue(){
            return 0; //通過(guò)initialValue方法設(shè)置默認(rèn)值
        }
    };

    public static void main(String[] args) {
        Thread[] threads=new Thread[5];
        for(int i=0;i<5;i++){
            threads[i]=new Thread(()->{
                int num=local.get().intValue();
                num+=5;
               System.out.println(Thread.currentThread().getName()+" : "+num);
            },"Thread-"+i);
        }

        for(Thread thread:threads){
            thread.start();
        }
    }
}

運(yùn)行結(jié)果

Thread-0 : 5
Thread-4 : 5
Thread-2 : 5
Thread-1 : 5
Thread-3 : 5

從結(jié)果可以看到,每個(gè)線程的值都是5加酵,意味著各個(gè)線程之間都是獨(dú)立的變量副本拳喻,彼此不相互影響.

ThreadLocal會(huì)給定一個(gè)初始值,也就是initialValue()方法猪腕,而每個(gè)線程都會(huì)從ThreadLocal中獲得這個(gè)初始化的值的副本冗澈,這樣可以使得每個(gè)線程都擁有一個(gè)副本拷貝

看到這里,估計(jì)有很多人都會(huì)和我一樣有一些疑問(wèn)

  1. 每個(gè)線程的變量副本是怎么存儲(chǔ)的?
  2. ThreadLocal是如何實(shí)現(xiàn)多線程場(chǎng)景下的共享變量副本隔離?

帶著疑問(wèn)陋葡,來(lái)看一下ThreadLocal這個(gè)類(lèi)的定義(默認(rèn)情況下,JDK的源碼都是基于1.8版本)

ThreadLocal的全局圖

從ThreadLocal的方法定義來(lái)看,還是挺簡(jiǎn)單的亚亲。就幾個(gè)方法

  • get: 獲取ThreadLocal中當(dāng)前線程對(duì)應(yīng)的線程局部變量
  • set:設(shè)置當(dāng)前線程的線程局部變量的值
  • remove:將當(dāng)前線程局部變量的值刪除

另外,還有一個(gè)initialValue()方法腐缤,在前面的代碼中有演示捌归,作用是返回當(dāng)前線程局部變量的初始值,這個(gè)方法是一個(gè)protected方法岭粤,主要是在構(gòu)造ThreadLocal時(shí)用于設(shè)置默認(rèn)的初始值

set方法的實(shí)現(xiàn)

set方法是設(shè)置一個(gè)線程的局部變量的值惜索,相當(dāng)于當(dāng)前線程通過(guò)set設(shè)置的局部變量的值,只對(duì)當(dāng)前線程可見(jiàn)剃浇。

    public void set(T value) {
        Thread t = Thread.currentThread();//獲取當(dāng)前執(zhí)行的線程
        ThreadLocalMap map = getMap(t); //獲得當(dāng)前線程的ThreadLocalMap實(shí)例
        if (map != null)//如果map不為空巾兆,說(shuō)明當(dāng)前線程已經(jīng)有了一個(gè)ThreadLocalMap實(shí)例
            map.set(this, value);//直接將當(dāng)前value設(shè)置到ThreadLocalMap中
        else
            createMap(t, value); //說(shuō)明當(dāng)前線程是第一次使用線程本地變量,構(gòu)造map
    }
  • Thread.currentThread 獲取當(dāng)前執(zhí)行的線程
  • getMap(t) ,根據(jù)當(dāng)前線程得到當(dāng)前線程的ThreadLocalMap對(duì)象虎囚,這個(gè)對(duì)象具體是做什么的?稍后分析
  • 如果map不為空角塑,說(shuō)明當(dāng)前線程已經(jīng)構(gòu)造過(guò)ThreadLocalMap,直接將值存儲(chǔ)到map中
  • 如果map為空淘讥,說(shuō)明是第一次使用圃伶,調(diào)用createMap構(gòu)造

ThreadLocalMap是什么?

我們來(lái)分析一下這句話,ThreadLocalMap map=getMap(t)獲得一個(gè)ThreadLocalMap對(duì)象,那這個(gè)對(duì)象是干嘛的呢?
其實(shí)不用分析窒朋,基本上也能猜測(cè)出來(lái)搀罢,Map是一個(gè)集合,集合用來(lái)存儲(chǔ)數(shù)據(jù)炼邀,那么在ThreadLocal中魄揉,應(yīng)該就是用來(lái)存儲(chǔ)線程的局部變量的。ThreadLocalMap這個(gè)類(lèi)很關(guān)鍵拭宁。

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

t.threadLocals實(shí)際上就是訪問(wèn)Thread類(lèi)中的ThreadLocalMap這個(gè)成員變量

public
class Thread implements Runnable {
 /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
...
}

從上面的代碼發(fā)現(xiàn)每一個(gè)線程都有自己?jiǎn)为?dú)的ThreadLocalMap實(shí)例,而對(duì)應(yīng)這個(gè)線程的所有本地變量都會(huì)保存到這個(gè)map內(nèi)

ThreadLocalMap是在哪里構(gòu)造?

set方法中瓣俯,有一行代碼createmap(t,value);杰标,這個(gè)方法就是用來(lái)構(gòu)造ThreadLocalMap,從傳入的參數(shù)來(lái)看彩匕,它的實(shí)現(xiàn)邏輯基本也能猜出出幾分吧

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

Thread t 是通過(guò)Thread.currentThread()來(lái)獲取的表示當(dāng)前線程腔剂,然后直接通過(guò)new ThreadLocalMap將當(dāng)前線程中的threadLocals做了初始化
ThreadLocalMap是一個(gè)靜態(tài)內(nèi)部類(lèi),內(nèi)部定義了一個(gè)Entry對(duì)象用來(lái)真正存儲(chǔ)數(shù)據(jù)

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

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            //構(gòu)造一個(gè)Entry數(shù)組驼仪,并設(shè)置初始大小
            table = new Entry[INITIAL_CAPACITY];
            //計(jì)算Entry數(shù)據(jù)下標(biāo)
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            //將`firstValue`存入到指定的table下標(biāo)中
            table[i] = new Entry(firstKey, firstValue);
            size = 1;//設(shè)置節(jié)點(diǎn)長(zhǎng)度為1
            setThreshold(INITIAL_CAPACITY); //設(shè)置擴(kuò)容的閾值
        }
    //...省略部分代碼
}

分析到這里掸犬,基本知道了ThreadLocalMap長(zhǎng)啥樣了,也知道它是如何構(gòu)造的?那么我看到這里的時(shí)候仍然有疑問(wèn)

  • Entry集成了WeakReference,這個(gè)表示什么意思?
  • 在構(gòu)造ThreadLocalMap的時(shí)候new ThreadLocalMap(this, firstValue);,key其實(shí)是this绪爸,this表示當(dāng)前對(duì)象的引用湾碎,在當(dāng)前的案例中,this指的是ThreadLocal<Integer> local奠货。那么多個(gè)線程對(duì)應(yīng)同一個(gè)ThreadLocal實(shí)例介褥,怎么對(duì)每一個(gè)ThreadLocal對(duì)象做區(qū)分呢?

解惑WeakReference

weakReference表示弱引用递惋,在Java中有四種引用類(lèi)型柔滔,強(qiáng)引用、弱引用萍虽、軟引用睛廊、虛引用。
使用弱引用的對(duì)象杉编,不會(huì)阻止它所指向的對(duì)象被垃圾回收器回收超全。

在Java語(yǔ)言中, 當(dāng)一個(gè)對(duì)象o被創(chuàng)建時(shí), 它被放在Heap里. 當(dāng)GC運(yùn)行的時(shí)候, 如果發(fā)現(xiàn)沒(méi)有任何引用指向o, o就會(huì)被回收以騰出內(nèi)存空間. 也就是說(shuō), 一個(gè)對(duì)象被回收, 必須滿足兩個(gè)條件:

  • 沒(méi)有任何引用指向它
  • GC被運(yùn)行.

這段代碼中,構(gòu)造了兩個(gè)對(duì)象a,b王财,a是對(duì)象DemoA的引用卵迂,b是對(duì)象DemoB的引用,對(duì)象DemoB同時(shí)還依賴對(duì)象DemoA绒净,那么這個(gè)時(shí)候我們認(rèn)為從對(duì)象DemoB是可以到達(dá)對(duì)象DemoA的见咒。這種稱(chēng)為強(qiáng)可達(dá)(strongly reachable)

DemoA a=new DemoA();
DemoB b=new DemoB(a);

如果我們?cè)黾右恍写a來(lái)將a對(duì)象的引用設(shè)置為null,當(dāng)一個(gè)對(duì)象不再被其他對(duì)象引用的時(shí)候挂疆,是會(huì)被GC回收的改览,但是對(duì)于這個(gè)場(chǎng)景來(lái)說(shuō)下翎,即時(shí)是a=null,也不可能被回收宝当,因?yàn)镈emoB依賴DemoA视事,這個(gè)時(shí)候是可能造成內(nèi)存泄漏的

DemoA a=new DemoA();
DemoB b=new DemoB(a);
a=null;

通過(guò)弱引用,有兩個(gè)方法可以避免這樣的問(wèn)題

//方法1
DemoA a=new DemoA();
DemoB b=new DemoB(a);
a=null;
b=null;
//方法2
DemoA a=new DemoA();
WeakReference b=new WeakReference(a);
a=null;

對(duì)于方法2來(lái)說(shuō)庆揩,DemoA只是被弱引用依賴俐东,假設(shè)垃圾收集器在某個(gè)時(shí)間點(diǎn)決定一個(gè)對(duì)象是弱可達(dá)的(weakly reachable)(也就是說(shuō)當(dāng)前指向它的全都是弱引用),這時(shí)垃圾收集器會(huì)清除所有指向該對(duì)象的弱引用订晌,然后把這個(gè)弱可達(dá)對(duì)象標(biāo)記為可終結(jié)(finalizable)的虏辫,這樣它隨后就會(huì)被回收。

試想一下如果這里沒(méi)有使用弱引用锈拨,意味著ThreadLocal的生命周期和線程是強(qiáng)綁定砌庄,只要線程沒(méi)有銷(xiāo)毀,那么ThreadLocal一直無(wú)法回收奕枢。而使用弱引用以后娄昆,當(dāng)ThreadLocal被回收時(shí),由于Entry的key是弱引用缝彬,不會(huì)影響ThreadLocal的回收防止內(nèi)存泄漏萌焰,同時(shí),在后續(xù)的源碼分析中會(huì)看到跌造,ThreadLocalMap本身的垃圾清理會(huì)用到這一個(gè)好處杆怕,方便對(duì)無(wú)效的Entry進(jìn)行回收

解惑ThreadLocalMap以this作為key

在構(gòu)造ThreadLocalMap時(shí),使用this作為key來(lái)存儲(chǔ)壳贪,那么對(duì)于同一個(gè)ThreadLocal對(duì)象陵珍,如果同一個(gè)Thread中存儲(chǔ)了多個(gè)值,是如何來(lái)區(qū)分存儲(chǔ)的呢违施?
答案就在firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)

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

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
}

關(guān)鍵點(diǎn)就在threadLocalHashCode互纯,它相當(dāng)于一個(gè)ThreadLocal的ID,實(shí)現(xiàn)的邏輯如下

private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode =
        new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

這里用到了一個(gè)非常完美的散列算法磕蒲,可以簡(jiǎn)單理解為留潦,對(duì)于同一個(gè)ThreadLocal下的多個(gè)線程來(lái)說(shuō),當(dāng)任意線程調(diào)用set方法存入一個(gè)數(shù)據(jù)到Entry中的時(shí)候辣往,其實(shí)會(huì)根據(jù)threadLocalHashCode生成一個(gè)唯一的id標(biāo)識(shí)對(duì)應(yīng)這個(gè)數(shù)據(jù)兔院,存儲(chǔ)在Entry數(shù)據(jù)下標(biāo)中。

  • threadLocalHashCode是通過(guò)nextHashCode.getAndAdd(HASH_INCREMENT)來(lái)實(shí)現(xiàn)的
    i*HASH_INCREMENT+HASH_INCREMENT,每次新增一個(gè)元素(ThreadLocal)到Entry[],都會(huì)自增0x61c88647,目的為了讓哈希碼能均勻的分布在2的N次方的數(shù)組里
  • Entry[i]= hashCode & (length-1)

魔數(shù)0x61c88647

從上面的分析可以看出站削,它是在上一個(gè)被構(gòu)造出的ThreadLocal的threadLocalHashCode的基礎(chǔ)上加上一個(gè)魔數(shù)0x61c88647坊萝。我們來(lái)做一個(gè)實(shí)驗(yàn),看看這個(gè)散列算法的運(yùn)算結(jié)果

    private static final int HASH_INCREMENT = 0x61c88647;
    public static void main(String[] args) {
        magicHash(16); //初始大小16
        magicHash(32); //擴(kuò)容一倍
    }

    private static void magicHash(int size){
        int hashCode = 0;
        for(int i=0;i<size;i++){
            hashCode = i*HASH_INCREMENT+HASH_INCREMENT;
            System.out.print((hashCode & (size-1))+" ");
        }
        System.out.println();
    }

輸出結(jié)果

7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0 
7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0 

根據(jù)運(yùn)行結(jié)果,這個(gè)算法在長(zhǎng)度為2的N次方的數(shù)組上十偶,確實(shí)可以完美散列菩鲜,沒(méi)有任何沖突, 是不是很神奇。

魔數(shù)0x61c88647的選取和斐波那契散列有關(guān)惦积,0x61c88647對(duì)應(yīng)的十進(jìn)制為1640531527接校。而斐波那契散列的乘數(shù)可以用(long) ((1L << 31) * (Math.sqrt(5) - 1)); 如果把這個(gè)值給轉(zhuǎn)為帶符號(hào)的int,則會(huì)得到-1640531527狮崩。也就是說(shuō)
(long) ((1L << 31) * (Math.sqrt(5) - 1));得到的結(jié)果就是1640531527蛛勉,也就是魔數(shù)0x61c88647

//(根號(hào)5-1)*2的31次方=(根號(hào)5-1)/2 *2的32次方=黃金分割數(shù)*2的32次方
long l1 = (long) ((1L << 31) * (Math.sqrt(5) - 1));
System.out.println("32位無(wú)符號(hào)整數(shù): " + l1);
int i1 = (int) l1;
System.out.println("32位有符號(hào)整數(shù):   " + i1);

總結(jié),我們用0x61c88647作為魔數(shù)累加為每個(gè)ThreadLocal分配各自的ID也就是threadLocalHashCode再與2的冪取模厉亏,得到的結(jié)果分布很均勻董习。

圖形分析

為了更直觀的體現(xiàn)set方法的實(shí)現(xiàn),通過(guò)一個(gè)圖形表示如下

set方法創(chuàng)造的ThreadLocalMap結(jié)構(gòu)

set剩余源碼分析

前面分析了set方法第一次初始化ThreadLocalMap的過(guò)程爱只,也對(duì)ThreadLocalMap的結(jié)構(gòu)有了一個(gè)全面的了解。那么接下來(lái)看一下map不為空時(shí)的執(zhí)行邏輯

private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            // 根據(jù)哈希碼和數(shù)組長(zhǎng)度求元素放置的位置招刹,即數(shù)組下標(biāo)
            int i = key.threadLocalHashCode & (len-1);
             //從i開(kāi)始往后一直遍歷到數(shù)組最后一個(gè)Entry(線性探索)
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                 //如果key相等恬试,覆蓋value
                if (k == key) {
                    e.value = value;
                    return;
                }
                 //如果key為null,用新key、value覆蓋疯暑,同時(shí)清理歷史key=null的陳舊數(shù)據(jù)
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
             //如果超過(guò)閥值训柴,就需要擴(kuò)容了
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

主要邏輯

  • 根據(jù)key的散列哈希計(jì)算Entry的數(shù)組下標(biāo)
  • 通過(guò)線性探索探測(cè)從i開(kāi)始往后一直遍歷到數(shù)組的最后一個(gè)Entry
  • 如果map中的key和傳入的key相等,表示該數(shù)據(jù)已經(jīng)存在妇拯,直接覆蓋
  • 如果map中的key為空幻馁,則用新的key、value覆蓋越锈,并清理key=null的數(shù)據(jù)
  • rehash擴(kuò)容

replaceStaleEntry

由于Entry的key為弱引用仗嗦,如果key為空,說(shuō)明ThreadLocal這個(gè)對(duì)象被GC回收了甘凭。
replaceStaleEntry的作用就是把陳舊的Entry進(jìn)行替換

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

           //向前掃描稀拐,查找最前一個(gè)無(wú)效的slot
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                   //通過(guò)循環(huán)遍歷,可以定位到最前面一個(gè)無(wú)效的slot
                    slotToExpunge = i; 

            //從i開(kāi)始往后一直遍歷到數(shù)組最后一個(gè)Entry(線性探索)
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                //找到匹配的key以后
                if (k == key) {
                    e.value = value;//更新對(duì)應(yīng)slot的value值
                    //與無(wú)效的sloat進(jìn)行交換
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    //如果最早的一個(gè)無(wú)效的slot和當(dāng)前的staleSlot相等丹弱,則從i作為清理的起點(diǎn)
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    //從slotToExpunge開(kāi)始做一次連續(xù)的清理
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

               
                //如果當(dāng)前的slot已經(jīng)無(wú)效德撬,并且向前掃描過(guò)程中沒(méi)有無(wú)效slot,則更新slotToExpunge為當(dāng)前位置
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            //如果key對(duì)應(yīng)的value在entry中不存在躲胳,則直接放一個(gè)新的entry
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

           //如果有任何一個(gè)無(wú)效的slot蜓洪,則做一次清理
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

cleanSomeSlots

這個(gè)函數(shù)有兩處地方會(huì)被調(diào)用,用于清理無(wú)效的Entry

  • 插入的時(shí)候可能會(huì)被調(diào)用
  • 替換無(wú)效slot的時(shí)候可能會(huì)被調(diào)用

區(qū)別是前者傳入的n為元素個(gè)數(shù)坯苹,后者為table的容量

private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                 // i在任何情況下自己都不會(huì)是一個(gè)無(wú)效slot隆檀,所以從下一個(gè)開(kāi)始判斷
                i = nextIndex(i, len);
                Entry e = tab[i];
                if (e != null && e.get() == null) {
                    n = len;// 擴(kuò)大掃描控制因子
                    removed = true;
                    i = expungeStaleEntry(i); // 清理一個(gè)連續(xù)段
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

expungeStaleEntry

執(zhí)行一次全量清理

private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;//刪除value
            tab[staleSlot] = null;//刪除entry
            size--; //map的size遞減

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);// 遍歷指定刪除節(jié)點(diǎn),所有后續(xù)節(jié)點(diǎn)
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {//key為null,執(zhí)行刪除操作
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {//key不為null,重新計(jì)算下標(biāo)
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {//如果不在同一個(gè)位置
                        tab[i] = null;//把老位置的entry置null(刪除)

                        // 從h開(kāi)始往后遍歷,一直到找到空為止刚操,插入
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

get操作

set的邏輯分析完成以后闸翅,get的源碼分析就很簡(jiǎn)單了

public T get() {
        Thread t = Thread.currentThread();
        //從當(dāng)前線程中獲取ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //查詢當(dāng)前ThreadLocal變量實(shí)例對(duì)應(yīng)的Entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {//獲取成功,直接返回
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果map為null,即還沒(méi)有初始化菊霜,走初始化方法
        return setInitialValue();
    }

setInitialValue

根據(jù)initialValue()的value初始化ThreadLocalMap

    private T setInitialValue() {
        T value = initialValue();//protected方法,用戶可以重寫(xiě)
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //如果map不為null,把初始化value設(shè)置進(jìn)去
            map.set(this, value);
        else
            //如果map為null,則new一個(gè)map,并把初始化value設(shè)置進(jìn)去
            createMap(t, value);
        return value;
    }
  • 從當(dāng)前線程中獲取ThreadLocalMap坚冀,查詢當(dāng)前ThreadLocal變量實(shí)例對(duì)應(yīng)的Entry,如果不為null,獲取value,返回
  • 如果map為null,即還沒(méi)有初始化鉴逞,走初始化方法

remove方法

remove的方法比較簡(jiǎn)單记某,從Entry[]中刪除指定的key就行

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

     private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();//調(diào)用Entry的clear方法
                    expungeStaleEntry(i);//清除陳舊數(shù)據(jù)
                    return;
                }
            }
        }

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

ThreadLocal的實(shí)際應(yīng)用場(chǎng)景:

  1. 比如在線程級(jí)別,維護(hù)session,維護(hù)用戶登錄信息userID(登陸時(shí)插入构捡,多個(gè)地方獲纫耗稀)
  2. 數(shù)據(jù)庫(kù)的鏈接對(duì)象Connection,可以通過(guò)ThreadLocal來(lái)做隔離避免線程安全問(wèn)題

問(wèn)題

ThreadLocal的內(nèi)存泄漏

ThreadLocalMap中Entry的key使用的是ThreadLocal的弱引用勾徽,如果一個(gè)ThreadLocal沒(méi)有外部強(qiáng)引用滑凉,當(dāng)系統(tǒng)執(zhí)行GC時(shí),這個(gè)ThreadLocal勢(shì)必會(huì)被回收喘帚,這樣一來(lái)畅姊,ThreadLocalMap中就會(huì)出現(xiàn)一個(gè)key為null的Entry,而這個(gè)key=null的Entry是無(wú)法訪問(wèn)的吹由,當(dāng)這個(gè)線程一直沒(méi)有結(jié)束的話若未,那么就會(huì)存在一條強(qiáng)引用鏈

圖片描述

Thread Ref - > Thread -> ThreadLocalMap - > Entry -> value 永遠(yuǎn)無(wú)法回收而造成內(nèi)存泄漏

其實(shí)我們從源碼分析可以看到,ThreadLocalMap是做了防護(hù)措施的

  • 首先從ThreadLocal的直接索引位置(通過(guò)ThreadLocal.threadLocalHashCode & (len-1)運(yùn)算得到)獲取Entry e倾鲫,如果e不為null并且key相同則返回e
  • 如果e為null或者key不一致則向下一個(gè)位置查詢粗合,如果下一個(gè)位置的key和當(dāng)前需要查詢的key相等,則返回對(duì)應(yīng)的Entry乌昔,否則隙疚,如果key值為null,則擦除該位置的Entry玫荣,否則繼續(xù)向下一個(gè)位置查詢

在這個(gè)過(guò)程中遇到的key為null的Entry都會(huì)被擦除甚淡,那么Entry內(nèi)的value也就沒(méi)有強(qiáng)引用鏈,自然會(huì)被回收捅厂。仔細(xì)研究代碼可以發(fā)現(xiàn)贯卦,set操作也有類(lèi)似的思想,將key為null的這些Entry都刪除焙贷,防止內(nèi)存泄露撵割。
但是這個(gè)設(shè)計(jì)一來(lái)與一個(gè)前提條件,就是調(diào)用get或者set方法辙芍,但是不是所有場(chǎng)景都會(huì)滿足這個(gè)場(chǎng)景的啡彬,所以為了避免這類(lèi)的問(wèn)題羹与,我們可以在合適的位置手動(dòng)調(diào)用ThreadLocal的remove函數(shù)刪除不需要的ThreadLocal,防止出現(xiàn)內(nèi)存泄漏

所以建議的使用方法是

  • 將ThreadLocal變量定義成private static的庶灿,這樣的話ThreadLocal的生命周期就更長(zhǎng)纵搁,由于一直存在ThreadLocal的強(qiáng)引用,所以ThreadLocal也就不會(huì)被回收往踢,也就能保證任何時(shí)候都能根據(jù)ThreadLocal的弱引用訪問(wèn)到Entry的value值腾誉,然后remove它,防止內(nèi)存泄露
  • 每次使用完ThreadLocal峻呕,都調(diào)用它的remove()方法利职,清除數(shù)據(jù)。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末瘦癌,一起剝皮案震驚了整個(gè)濱河市猪贪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌讯私,老刑警劉巖热押,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異斤寇,居然都是意外死亡楞黄,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)抡驼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人肿仑,你說(shuō)我怎么就攤上這事致盟。” “怎么了尤慰?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵馏锡,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我伟端,道長(zhǎng)杯道,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任责蝠,我火速辦了婚禮党巾,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘霜医。我一直安慰自己齿拂,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布肴敛。 她就那樣靜靜地躺著署海,像睡著了一般吗购。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上砸狞,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天捻勉,我揣著相機(jī)與錄音,去河邊找鬼刀森。 笑死踱启,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的撒强。 我是一名探鬼主播禽捆,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼飘哨!你這毒婦竟也來(lái)了胚想?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤芽隆,失蹤者是張志新(化名)和其女友劉穎浊服,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體胚吁,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡牙躺,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了腕扶。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片孽拷。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖半抱,靈堂內(nèi)的尸體忽然破棺而出脓恕,到底是詐尸還是另有隱情,我是刑警寧澤窿侈,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布炼幔,位于F島的核電站,受9級(jí)特大地震影響史简,放射性物質(zhì)發(fā)生泄漏乃秀。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一圆兵、第九天 我趴在偏房一處隱蔽的房頂上張望跺讯。 院中可真熱鬧,春花似錦衙傀、人聲如沸抬吟。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)火本。三九已至危队,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間钙畔,已是汗流浹背茫陆。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留擎析,地道東北人簿盅。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像揍魂,于是被迫代替她去往敵國(guó)和親桨醋。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

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

  • 前言 ThreadLocal很多同學(xué)都搞不懂是什么東西现斋,可以用來(lái)干嘛喜最。但面試時(shí)卻又經(jīng)常問(wèn)到,所以這次我和大家一起學(xué)...
    懶癌正患者閱讀 1,123評(píng)論 1 34
  • 前言 ThreadLocal很多同學(xué)都搞不懂是什么東西庄蹋,可以用來(lái)干嘛瞬内。但面試時(shí)卻又經(jīng)常問(wèn)到,所以這次我和大家一起學(xué)...
    liangzzz閱讀 12,445評(píng)論 14 228
  • 一限书、使用姿勢(shì) 二虫蝶、數(shù)據(jù)結(jié)構(gòu) 三、源碼分析 四倦西、回收機(jī)制 總結(jié) 一能真、使用姿勢(shì) 最佳實(shí)踐 在類(lèi)中定義ThreadLoc...
    原水寒閱讀 1,587評(píng)論 2 8
  • Android Handler機(jī)制系列文章整體內(nèi)容如下: Android Handler機(jī)制1之ThreadAnd...
    隔壁老李頭閱讀 7,634評(píng)論 4 30
  • 他們說(shuō)去珍爐吃飯,我想起了我們一起在那兒吃的那次扰柠。 我覺(jué)得你現(xiàn)在都不知道舟陆,我當(dāng)時(shí)喝多了一直嘟囔的那句“你什么都不知...
    蓬蓬蓬的毛毛熊閱讀 212評(píng)論 4 0