并發(fā)與多線程-ThreadLocal

??ThreadLocal 初衷是在線程并發(fā)時(shí)犹芹,解決變量共享問(wèn)題顷帖,但由于過(guò)度設(shè)計(jì)臀脏,比如弱引用和哈希碰撞,導(dǎo)致理解難度大墓怀、使用成本高,反而成為故障高發(fā)點(diǎn)卫键,容易出現(xiàn)內(nèi)存泄漏傀履、臟數(shù)據(jù)、共享對(duì)象更新等問(wèn)題莉炉。

1. 引用類型

??對(duì)象在堆上創(chuàng)建之后所持有的引用其實(shí)是一種變量類型钓账,引用之間可以通過(guò)賦值構(gòu)成一條引用鏈。從GCRoots 開始遍歷呢袱,判斷引用是否可達(dá)官扣。引用的可達(dá)性是判斷能否被垃圾回收的基本條件。JVM 會(huì)據(jù)此自動(dòng)管理內(nèi)存的分配與回收羞福,不需要開發(fā)工程師干預(yù)惕蹄。但在某些場(chǎng)景下,即使引用可達(dá)治专,也希望能夠根據(jù)語(yǔ)義的強(qiáng)弱進(jìn)行有選擇的回收卖陵,以保證系統(tǒng)的正常運(yùn)行。根據(jù)引用類型語(yǔ)義的強(qiáng)弱來(lái)決定垃圾回收的階段张峰,我們可以把引用分為強(qiáng)引用泪蔫、軟引用、弱引用和虛引用四類喘批。后三類引用撩荣,本質(zhì)上是可以讓開發(fā)工程師通過(guò)代碼方式來(lái)決定對(duì)象的垃圾回收時(shí)機(jī)。我們先簡(jiǎn)要了解一下這四類引用饶深。

  • 強(qiáng)引用餐曹,即 Strong Reference,最為常見敌厘。
    如 Object object = new Object(); 這樣的變量聲明和定義就會(huì)產(chǎn)生對(duì)該對(duì)象的強(qiáng)引用台猴。只要對(duì)象有強(qiáng)引用指向,并且 GC Roots可達(dá)俱两,那么 Java 內(nèi)存回收時(shí)饱狂,即使瀕臨內(nèi)存耗盡,也不會(huì)回收該對(duì)象宪彩。
  • 軟引用休讳,即 Sof Reference
    引用力度弱于“強(qiáng)引用”,是用在非必需對(duì)象的場(chǎng)景在即將 OOM 之前尿孔,垃圾回收器會(huì)把這些軟引用指向的對(duì)象加入回收范圍衍腥,以獲得更多的內(nèi)存空間磺樱,讓程序能夠繼續(xù)健康運(yùn)行。主要用來(lái)緩存服務(wù)器中間計(jì)算結(jié)果及不需要實(shí)時(shí)保存的用戶行為等婆咸。
  • 弱引用,即 Weak Reference
    引用強(qiáng)度較前兩者更弱芜辕,也是用來(lái)描述非必需對(duì)象的尚骄。如果弱引用指向的對(duì)象只存在弱引用這一條線路,則在下一次YGC 時(shí)會(huì)被回收侵续。由于 YGC 時(shí)間的不確定性倔丈,弱引用何時(shí)被回收也具有不確定性。弱引用主要用于指向某個(gè)易消失的對(duì)象状蜗,在強(qiáng)引用斷開后需五,此引用不會(huì)劫持對(duì)象。調(diào)用 WeakReference.get()可能返回null轧坎,要注意空指針異常宏邮。
  • 虛引用,即 Phantom Reference
    是極弱的一種引用關(guān)系缸血,定義完成后蜜氨,就無(wú)法通過(guò)該引用獲取指向的對(duì)象。為一個(gè)對(duì)象設(shè)置虛引用的唯一目的就是希望能在這個(gè)對(duì)象被回收時(shí)收到一個(gè)系統(tǒng)通知捎泻。虛引用必須與引用隊(duì)列聯(lián)合使用飒炎,當(dāng)垃圾回收時(shí),如果發(fā)現(xiàn)存在虛引用笆豁,就會(huì)在回收對(duì)象內(nèi)存前郎汪,把這個(gè)虛引用加入與之關(guān)聯(lián)的引用隊(duì)列中。
    對(duì)象的引用類型如圖所示闯狱。
    對(duì)象引用類型

    ??舉個(gè)具體例子煞赢,在房產(chǎn)交易市場(chǎng)中,某個(gè)賣家有一套房子扩氢,成功出售賣給某個(gè)買家后引用置為 null耕驰。這里有4個(gè)買家使用4種不同的引用關(guān)系指向這套房子。買家buyer1是強(qiáng)引用录豺,如果把 seller 引用賦值給它朦肘,則永久有效,系統(tǒng)不會(huì)因?yàn)?seller=null就觸發(fā)對(duì)這套房子的回收双饥,這是房屋交易市場(chǎng)最常見的交付方式媒抠。買家 buyer2 是軟引用,只要不產(chǎn)生OOM咏花,buyer2.get() 就可以獲取房子對(duì)象趴生,就像房子是租來(lái)的一樣阀趴。買家 buyer3 是弱引用,一旦過(guò)戶后苍匆,seller 置為 null刘急,buyer3 的房子持有時(shí)間估計(jì)只有幾秒鐘,賣家只是給買家做了一張假的房產(chǎn)證浸踩,買家高興了幾秒鐘后叔汁,發(fā)現(xiàn)房子已經(jīng)不是自己的了。buyer4 是虛引用检碗,定義完成后無(wú)法訪問(wèn)到房子對(duì)象据块,賣家只是虛構(gòu)了房源,是空手套白狼的詐騙術(shù)折剃。
    ??強(qiáng)引用是最常用的另假,而虛引用在業(yè)務(wù)中幾乎很難用到。本文重點(diǎn)介紹一下軟引用和弱引用怕犁。先來(lái)說(shuō)明一下軟引用的回收機(jī)制边篮。首先設(shè)置JVM參數(shù):-Xms20mXmx20m,即只有50MB 的堆內(nèi)存空間因苹。在下方的示例代碼中不斷地往集合里添加House 對(duì)象苟耻,而每個(gè)House 有2000個(gè)Door 成員變量,狹小的堆空間加上大對(duì)象的產(chǎn)生扶檐,就是為了盡快觸達(dá)內(nèi)存耗盡的臨界狀態(tài):
    idea 修改vm參數(shù): Run > Edit Configurations>Appliaction 找到應(yīng)用類 在右側(cè) VM options: 填寫 -Xms50m -Xmx50m
public class SoftReferenceHouse {
    public static void main(String[] args) {
        //List<House> houses = new ArrayList<House>(); 第1處
        List<SoftReference> houses = new ArrayList<SoftReference>();

        int i = 0;
        while (true) {
            // houses.add(new House()); 第2處
            //劇情反轉(zhuǎn)注釋處
            SoftReference<House> buyer2 = new SoftReference<House>(new House());
            //劇情發(fā)展注釋處
            houses.add(buyer2);
            System.out.println("i=" + (++i));
        }
    }
}

class House {
    private static final Integer DOOR_NUMBER = 2000;
    public Door[] doors = new Door[DOOR_NUMBER];

    class Door {
    }
}

new House0是匿名對(duì)象凶杖,產(chǎn)生之后即賦值給軟引用。正常運(yùn)行一段時(shí)間后款筑,內(nèi)存到達(dá)耗盡的臨界狀態(tài)智蝠,House$Door超過(guò)24MB左右,內(nèi)存占比達(dá)到50%奈梳,(可通過(guò)visualvm 查看杈湾,jconsole也可)


軟應(yīng)用下的對(duì)象堆積

軟引用的特性在數(shù)秒之后產(chǎn)生價(jià)值,House對(duì)象數(shù)從千數(shù)量級(jí)迅速降到百數(shù)量級(jí)內(nèi)存容量迅速被釋放出來(lái)攘须,保證了程序的正常運(yùn)行漆撞,


OOM 時(shí)軟引用觸發(fā)對(duì)象回收

??軟引用 SoftReference 的父類 Reference 的屬性: private T referent,它指向 newHouse()對(duì)象于宙,而SoftReference 的 get()浮驳,也是調(diào)用了 super.get() 來(lái)訪問(wèn)父類這個(gè)私有屬性。大量的 House 在內(nèi)存即將耗盡前捞魁,成功地一次又一次被清理掉至会。對(duì)象 buyer2雖然是引用類型,但其本身還是占用一定內(nèi)存空間的谱俭,它是被集合 ArrayList強(qiáng)引用劫持的奉件。在不斷循環(huán)執(zhí)行 houses.add(),后在=566581時(shí)宵蛀,終于產(chǎn)生了 OOM。軟引用县貌、弱引用术陶、虛引用均存在帶有隊(duì)列的構(gòu)造方法:
??public SoftReference(T referent, ReferenceQueue<? super T> 9){...}

??可以在隊(duì)列中檢查哪個(gè)軟引用的對(duì)象被回收了,從而把失去 House 的軟引用對(duì)象清理掉煤痕。
??反轉(zhuǎn)一下劇情瞳别。在同一個(gè)類中,使用完全相同的運(yùn)行環(huán)境和內(nèi)存參數(shù)杭攻,把SoftReference<House>中被注釋掉的兩句代碼激活( 即示例代碼中的第1處和第2處)同時(shí)把在后邊標(biāo)記了“劇情反轉(zhuǎn)注釋處”的3 句代碼注釋掉,再次運(yùn)行疤坝。觀察一下兆解,在沒(méi)有軟引用的情況下,這個(gè)循環(huán)能夠撐多久?運(yùn)行得到的結(jié)果在i=2404 時(shí)跑揉,就產(chǎn)生 OOM 異常锅睛。這個(gè)示例簡(jiǎn)單地證明了軟引用在內(nèi)存緊張情況下的回收能力。軟引用一般用于在同一服務(wù)器內(nèi)緩存中間結(jié)果历谍。如果命中緩存现拒,則提取緩存結(jié)果,否則重新計(jì)算或獲取望侈。但是印蔬,軟引用肯定不是用來(lái)緩存高頻數(shù)據(jù)的,萬(wàn)一服務(wù)器重啟或者軟引用觸發(fā)大規(guī)耐蜒茫回收侥猬,所有的訪問(wèn)將直接指向數(shù)據(jù)庫(kù),導(dǎo)致數(shù)據(jù)庫(kù)的壓力時(shí)大時(shí)小捐韩,甚至崩潰退唠。
如果內(nèi)存沒(méi)有達(dá)到OOM,軟引用持有的對(duì)象會(huì)被回收嗎?下面用代碼來(lái)驗(yàn)證一下

public class SoftReferenceWhenIdle {
    public static void main(String[] args) {
        House seller = new House();
        //(第1處)
        SoftReference<House> buyer2 = new SoftReference<House>(seller);
        seller = null;
        while (true) {
            // 下方兩句代碼建議 JVM 進(jìn)行垃圾回收
            System.gc();
            System.runFinalization();
            if (buyer2.get() == null) {
                System.out.println("house is null");
                break;
            } else {
                System.out.println("still there.");
            }
        }
    }
}

??System.gc()方法建議垃圾收集器盡快進(jìn)行垃圾收集,具體何時(shí)執(zhí)行仍由JVM來(lái)
判斷荤胁。System.runFinalization0方法的作用是強(qiáng)制調(diào)用已經(jīng)失去引用對(duì)象的finalize()瞧预。
??在代碼中同時(shí)調(diào)用這兩者,有利于更快地執(zhí)行垃圾回收仅政。在相同的運(yùn)行環(huán)境下垢油,一直輸出stillthere,說(shuō)明 buyer2一直持有new House()的有效引用已旧。如果在對(duì)方置頭null 時(shí)仍能自動(dòng)感知秸苗,并且主動(dòng)斷開引用指向的對(duì)象,這是哪種引用方式可以擔(dān)負(fù)的使命?答案是弱引用运褪。事實(shí)上惊楼,把示例代碼中第1處的兩個(gè)紅色SoftReference 修改為 WeakReference 即可實(shí)現(xiàn)回收玖瘸。出于對(duì) WeakReference 的尊重,摒棄剛才催促垃圾回收的代碼檀咙,讓W(xué)eakReference 自然地被YGC 回收雅倒,使對(duì)象能夠存活更長(zhǎng)的時(shí)間我們可以在JVM啟動(dòng)參數(shù)加-XX:+PrintGCDetails(或高版本JDK使用-Xlog:gc)來(lái)觀察GC的觸發(fā)情況:

public class WeakReferenceWhenIdle {
    public static void main(String[] args) {
        House seller = new House();
        WeakReference<House> buyer3 = new WeakReference<House>(seller);

        long start = System.nanoTime();
        int count = 0;
        while (true) {
            if (buyer3.get() == null) {
                long duration = (System.nanoTime() - start) / (1000 * 1000);
                System.out.println("house is null and exited time=" + duration + "ms");
            } else {
                System.out.println("still there. count =" + (count++));
            }
        }
    }
}

執(zhí)行結(jié)果如下:
still there.count = 232639
[GC [PSYoungGen: 65536K->688K(76288K)] 65536K->696K(251392K), 0.0074719secs] [Times: user=0.01 sys-0.00弧可,real-0.01 secs]
still there. count = 232640
house is null and exited time = 1013ms

2. ThreadLocal價(jià)值

??我們從真人 CS 游戲說(shuō)起蔑匣。游戲開始時(shí),每個(gè)人能夠領(lǐng)到一把電子槍棕诵,槍把上有三個(gè)數(shù)字:子彈數(shù)裁良、殺敵數(shù)、自己的命數(shù)校套,為其設(shè)置的初始值分別為 1500价脾、0、10笛匙。假設(shè)戰(zhàn)場(chǎng)上的每個(gè)人都是一個(gè)線程侨把,那么這三個(gè)初始值寫在哪里呢?如果每個(gè)線程寫死這三個(gè)值,萬(wàn)一將初始子彈數(shù)統(tǒng)一改成 1000 發(fā)呢?如果共享妹孙,那么線程之間的并發(fā)修改會(huì)導(dǎo)致數(shù)據(jù)不準(zhǔn)確秋柄。能不能構(gòu)造這樣一個(gè)對(duì)象,將這個(gè)對(duì)象設(shè)置為共享變量蠢正,統(tǒng)一設(shè)置初始值骇笔,但是每個(gè)線程對(duì)這個(gè)值的修改都是互相獨(dú)立的。這個(gè)對(duì)象就是ThreadLocal机隙。注意不能將其翻譯為線程本地化或本地線程蜘拉,英語(yǔ)恰當(dāng)?shù)拿Q應(yīng)該叫作CopyValueIntoEveryThread。具體示例代碼如下:

public class CsGameByThreadLocal {
    private static final Integer BULLET_NUMBER = 1500;
    private static final Integer KILLED_ENEMIES = 0;

    private static final Integer LIFE_VALUE = 10;
    private static final Integer TOTAL_PLAYERS = 10;
    // 隨機(jī)數(shù)用來(lái)展示每個(gè)對(duì)鼻的不同的數(shù)據(jù)(第1處)
    private static final ThreadLocalRandom RANDOM = ThreadLocalRandom.current();

    //初始化子彈數(shù)
    private static final ThreadLocal<Integer> BULLET_NUMBER_THREADLOCAL = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return BULLET_NUMBER;
        }
    };
    //初始化殺敵數(shù)
    private static final ThreadLocal<Integer> KILLED_ENEMIES_THREADLOCAL = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return KILLED_ENEMIES;
        }
    };
    //初始化自己的明叔
    private static final ThreadLocal<Integer> LIFE_VALUE_THREADLOCAL = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return LIFE_VALUE;
        }
    };

    //定義每位隊(duì)員
    private static class Player extends Thread {
        @Override
        public void run() {
            Integer bullets = BULLET_NUMBER_THREADLOCAL.get() - RANDOM.nextInt(BULLET_NUMBER);

            Integer killEnemies = KILLED_ENEMIES_THREADLOCAL.get() + RANDOM.nextInt(TOTAL_PLAYERS / 2);

            Integer lifeValue = LIFE_VALUE_THREADLOCAL.get() - RANDOM.nextInt(LIFE_VALUE);

            System.out.println(getName() + ",BULLET NUMBER is " + bullets);
            System.out.println(getName() + ",KILLED_ENEMIES is " + killEnemies);
            System.out.println(getName() + ",LIEF_VALUES is " + lifeValue + "\n");

            BULLET_NUMBER_THREADLOCAL.remove();
            KILLED_ENEMIES_THREADLOCAL.remove();
            LIFE_VALUE_THREADLOCAL.remove();

        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < TOTAL_PLAYERS; i++) {
            new Player().start();
        }
    }
}

??此示例中有鹿,沒(méi)有進(jìn)行 set 操作旭旭,那么初始值又是如何進(jìn)入每個(gè)線程成為獨(dú)立撓貝的呢?首先,雖然 ThreadLocal 在定義時(shí)覆寫了initialValue()方法葱跋,但并非是在BULLET_NUMBER_THREADLOCAL對(duì)象加載靜態(tài)變量的時(shí)候執(zhí)行的持寄,而是每個(gè)線程在ThreadLocal.get()的時(shí)候都會(huì)執(zhí)行到,其源碼如下

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

??每個(gè)線程都有自己的ThreadLocalMap娱俺,如果map==null稍味,則直接執(zhí)行setInitialValue()。如果map 已經(jīng)創(chuàng)建荠卷,就表示Thread類的threadLocals屬性已經(jīng)初始化:如果e==null模庐,依然會(huì)執(zhí)行到 setInitialValue0。setInitialValue()的源碼如下

    protected T initialValue() {
        return null;
    }
    private T setInitialValue() {
      // 這是一個(gè)保護(hù)方法油宜,CsGameByThreadLocal 中初始化ThreadLocal 對(duì)象時(shí)已覆寫
        T value = initialValue();
        Thread t = Thread.currentThread();
      // getMap 的源碼就是提取線程對(duì)象t的ThreadLocalMap 屬性:t.threadLocals
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        return value;
    }

??在 CsGameByThreadLocal 類的第 1處掂碱,使用了ThreadLocalRandom 生成單獨(dú)的Random 實(shí)例怜姿。此類在JDK7 中引入,它使得每個(gè)線程都可以有自己的隨機(jī)數(shù)生成器疼燥。我們要避免 Random實(shí)例被多線程使用沧卢,雖然共享該實(shí)例是線程安全的,但會(huì)因競(jìng)爭(zhēng)同一seed 而導(dǎo)致性能下降醉者。
??我們已經(jīng)知道了 ThreadLocal是每一個(gè)線程單獨(dú)持有的但狭。因?yàn)槊恳粋€(gè)線程都有獨(dú)立的變量副本,其他線程不能訪問(wèn)撬即,所以不存在線程安全問(wèn)題立磁,也不會(huì)影響程序的執(zhí)行性能。ThreadLocal 對(duì)象通常是由 private static 修飾的剥槐,因?yàn)槎夹枰獜?fù)制進(jìn)入本地線程息罗,所以非 static作用不大。需要注意的是,ThreadLocal無(wú)法解決共享對(duì)象的更新問(wèn)題,下面的代碼實(shí)例將證明這點(diǎn)才沧。因?yàn)?CsGameByThreadLocal中使用的是Integer 的不可變對(duì)象,所以可以使用相同的編碼方式來(lái)操作一下可變對(duì)象看看绍刮,示例源碼如下:

public class InitValueInThreadLocal {
    private static final StringBuilder INIT_VALUE = new StringBuilder("init");

    //覆寫ThreadLocal的initalValue温圆、返回StringBuilder靜態(tài)引用
    private static final ThreadLocal<StringBuilder> builder = new ThreadLocal<StringBuilder>() {
        @Override
        protected StringBuilder initialValue() {
            return INIT_VALUE;
        }
    };

    private static class AppendStringThread extends Thread {
        @Override
        public void run() {
            StringBuilder inThread = builder.get();
            for (int i = 0; i < 10; i++) {
                inThread.append("_" + i);
            }
            System.out.println(inThread.toString());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            new AppendStringThread().start();
        }
        TimeUnit.SECONDS.sleep(10);
    }
}

??輸出的結(jié)果是亂序不可控的,所以使用某個(gè)引用來(lái)操作共享對(duì)象時(shí)孩革,依然需要進(jìn)行線程同步岁歉。
??ThreadLocal 有個(gè)靜態(tài)內(nèi)部類叫 ThreadLocalMap,它還有一個(gè)靜態(tài)內(nèi)部類叫 Entry膝蜈, 在 Thread 中的 ThreadLocalMap 屬性的賦值是在 ThreadLocal 類中的createMap() 中進(jìn)行的锅移。ThreadLocal 與 ThreadLocalMap 有三組對(duì)應(yīng)的方法: get()、set()和 remove0饱搏,在ThreadLocal中對(duì)它們只做校驗(yàn)和判斷非剃,最終的實(shí)現(xiàn)會(huì)落在ThreadLocalMap上。Entry繼承自 WeakReference,沒(méi)有方法推沸,只有一個(gè)value 成員變量它的key是ThreadLocal對(duì)象备绽。

  • 1個(gè) Thread 有且僅有1個(gè) ThreadLocalMap 對(duì)象;
  • 1個(gè) Entry 對(duì)象的 Key 弱引用指向1個(gè) ThreadLocal 對(duì)象;
  • 1個(gè) ThreadLocalMap 對(duì)象存儲(chǔ)多個(gè)Entry 對(duì)象
  • 1個(gè)ThreadLocal對(duì)象可以被多個(gè)線程所共享
  • ThreadLocal對(duì)象不持有 Value,Value 由線程的 Entry 對(duì)象持有鬓催。
    ??最后肺素,SimpleDateFormat 是線程不安全的類,定義static 對(duì)象宇驾,會(huì)有數(shù)據(jù)同步風(fēng)險(xiǎn)倍靡。通過(guò)源碼可以看出,SimpleDateFormat 內(nèi)部有一個(gè) Calendar 對(duì)象课舍,在日期轉(zhuǎn)字符串或字符串轉(zhuǎn)日期的過(guò)程中塌西,多線程共享時(shí)有非常高的概率產(chǎn)生錯(cuò)誤他挎,推薦的方式之一就是使用 ThreadLocal,讓每個(gè)線程單獨(dú)擁有這個(gè)對(duì)象雨让。示例代碼如下:
    private static final ThreadLocal<DateFormat> DATE_FORMAT_THREADLOCAL = new ThreadLocal<DateFormat>(){
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };

3.ThreadLocal副作用

??為了使線程安全地共享某個(gè)變量雇盖,JDK 開出了 ThreadLocal 這劑藥方。但“是藥三分毒”栖忠,ThreadLocal有一定的副作用崔挖,所以需要仔細(xì)閱讀藥方說(shuō)明書,了解藥性和注意事項(xiàng)庵寞。ThreadLocal 的主要問(wèn)題是會(huì)產(chǎn)塵數(shù)據(jù)和內(nèi)存泄漏狸相。這兩個(gè)問(wèn)題通常是在線程池的線程中使用 ThreadLocal 引發(fā)的,因?yàn)榫€程范有線程復(fù)用和內(nèi)存常駐兩個(gè)特點(diǎn)捐川。
??1.臟數(shù)據(jù)
??線程復(fù)用會(huì)產(chǎn)生臟數(shù)據(jù)脓鹃。由于線程池會(huì)重用 Thread 對(duì)象,那么與 Thread 綁定的類的靜態(tài)屬性 ThreadLocal 變量也會(huì)被重用古沥。如果在實(shí)現(xiàn)的線程 run()方法體中不顯式地調(diào)用remove()清理與線程相關(guān)的 ThreadLocal信息瘸右,那么倘若下一個(gè)線程不調(diào)用set()設(shè)置初始值,就可能 get()到重用的線程信息岩齿,包括 ThreadLocal 所關(guān)聯(lián)的線程對(duì)象的 value 值太颤。
??臟數(shù)據(jù)問(wèn)題在實(shí)際故障中十分常見。比如盹沈,用戶 A 下單后沒(méi)有看到訂單記錄龄章,而用戶 B 卻看到了用戶A 的訂單記錄。通過(guò)排查發(fā)現(xiàn)是由于 session 優(yōu)化引發(fā)的乞封。在原來(lái)的請(qǐng)求過(guò)程中做裙,用戶每次請(qǐng)求 Server,都需要通過(guò) sessionId 去緩存里查詢用戶的session 信息肃晚,這樣做無(wú)疑增加了一次調(diào)用锚贱。因此,開發(fā)工程師決定采用某框架來(lái)緩存每個(gè)用戶對(duì)應(yīng)的 SecurityContext关串,它封裝了 session 相關(guān)信息惋鸥。優(yōu)化后雖然會(huì)為每個(gè)用戶新建一個(gè)session 相關(guān)的上下文,但是由于Threadlocal沒(méi)有在線程處理結(jié)束時(shí)及時(shí)進(jìn)行 remove()清理操作悍缠,在高并發(fā)場(chǎng)景下卦绣,線程池中的線程可能會(huì)讀取到上一個(gè)線程緩存的用戶信息。為了便于理解飞蚓,用一段簡(jiǎn)要代碼來(lái)模擬滤港,如下所示:

public class DirtyDataInThreadLocal {
    public static ThreadLocal<String> threadLocal = new ThreadLocal<String>();

    public static void main(String[] args) {
        // 使用固定大小為1的線程池,說(shuō)明上一個(gè)的線程屬性會(huì)被下一個(gè)線程屬性復(fù)用
        ExecutorService pool = Executors.newFixedThreadPool(1);
        for (int i = 0; i < 2; i++) {
            Mythead mythead = new Mythead();
            pool.execute(mythead);
        }
    }

    private static class Mythead extends Thread {
        private static boolean flag = true;

        @Override
        public void run() {
            if (flag) {
                //第1個(gè)線程set后,并沒(méi)有進(jìn)行remove
                //而第二個(gè)線程由于某種原因沒(méi)有進(jìn)行set 操作
                threadLocal.set(getName() + ". session info.");
                flag = false;
            }
            System.out.println(getName() + " 線程是 " + threadLocal.get());
        }
    }
}

執(zhí)行結(jié)果如下:
Thread-0 線程是 Thread-0. session info.
Thread-1 線程是 Thread-0. session info.
??2.內(nèi)存泄漏
??在源碼注釋中提示使用 static 關(guān)鍵字來(lái)修飾 ThreadLocal溅漾。在此場(chǎng)景下山叮,寄希望于ThreadLocal對(duì)象失去引用后,觸發(fā)弱引用機(jī)制來(lái)回收 Entry 的 Value就不現(xiàn)實(shí)了添履。在上例中屁倔,如果不進(jìn)行 remove()操作,那么這個(gè)線程執(zhí)行完成后暮胧,通過(guò)ThreadLocal對(duì)象持有的String對(duì)象是不會(huì)被釋放的锐借。
??以上兩個(gè)問(wèn)題的解決辦法很簡(jiǎn)單,就是在每次用完 ThreadLocal時(shí)往衷,必須要及時(shí)調(diào)用remove()方法清理钞翔。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市席舍,隨后出現(xiàn)的幾起案子布轿,更是在濱河造成了極大的恐慌,老刑警劉巖来颤,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件汰扭,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡福铅,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)鲁冯,“玉大人,你說(shuō)我怎么就攤上這事薯演∽采郑” “怎么了跨扮?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)衡创。 經(jīng)常有香客問(wèn)我帝嗡,道長(zhǎng),這世上最難降的妖魔是什么璃氢? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮巢寡,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘抑月。我一直安慰自己谦絮,他們只是感情好题诵,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布仇轻。 她就那樣靜靜地躺著奶甘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪疲陕。 梳的紋絲不亂的頭發(fā)上钉赁,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音诅岩,去河邊找鬼带膜。 笑死,一個(gè)胖子當(dāng)著我的面吹牛式廷,可吹牛的內(nèi)容都是我干的芭挽。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼袜爪,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了辛馆?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤倔韭,失蹤者是張志新(化名)和其女友劉穎寿酌,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體醇疼,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡秧荆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年乙濒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片颁股。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡甘有,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出亏掀,到底是詐尸還是另有隱情滤愕,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站韭畸,受9級(jí)特大地震影響胰丁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜锦庸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望萝嘁。 院中可真熱鬧,春花似錦酸钦、人聲如沸咱枉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)亿乳。三九已至,卻和暖如春河爹,著一層夾襖步出監(jiān)牢的瞬間桐款,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工媳维, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留遏暴,地道東北人朋凉。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像墓毒,于是被迫代替她去往敵國(guó)和親亲怠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

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