ThreadLocal初探

摘要:ThreadLocal是并發(fā)場景下用來解決變量共享問題的類轴合,它能使原本線程間共享的對象進(jìn)行線程隔離奔穿,即一個對象只對一個線程可見耘沼。但由于過度設(shè)計碌冶,比如使用弱引用和哈希碰撞庸追,導(dǎo)致理解難度大霍骄、使用成本高,反而成為故障高發(fā)點淡溯,容易出現(xiàn)內(nèi)存泄漏读整、臟數(shù)據(jù)、共享對象更新等問題咱娶。

ThreadLocal原理解析與注意事項

關(guān)鍵字:ThreadLocal米间、ThreadLcoalMap强品、HashCode、1640531527屈糊、AtomicInteger的榛、CAS、ABA問題逻锐。

一夫晌、基礎(chǔ)實驗

package threadlocal;

import root.Log;

public class ThreadLocalVar {
    private static final String TAG = "ThreadLocalVar";

    public static void main(String[] args) {
        ThreadLocal<String> value01 = new ThreadLocal<>();
        value01.set("hello world! in 01");

        Log.i(TAG, Thread.currentThread().getName() + ": " + value01.get());

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);

                    ThreadLocal<String> value02 = new ThreadLocal<>();
                    value02.set("hello world! in 02");

                    Log.i(TAG, Thread.currentThread().getName() + ": " + value01.get());
                    Log.i(TAG, Thread.currentThread().getName() + ": " + value02.get());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}
[ThreadLocalVar] main: hello world! in 01
[ThreadLocalVar] Thread-0: null
[ThreadLocalVar] Thread-0: hello world! in 02

結(jié)論:

  • 不同的線程不可以共享變量。例如上述<code>value01</code>在子線程中不可見昧诱。
  • 通過使用<code>ThreadLocal</code>為鍵晓淀,在當(dāng)前線程中存儲一個對象。

二盏档、Q&T&A

通過閱讀源碼部分凶掰,可以知道每個線程都會存儲一個<code>ThreadLocalMap</code>,來維護(hù)當(dāng)前線程的<code>ThreadLocal</code>對象蜈亩。

1. <code>main</code>方法主線程的ThreadLocalMap

Q1:通過<code>main</code>方法啟動的線程懦窘,其是否有初始化<code>ThreadLocalMap</code>?
T1:測試代碼
  • 在<code>main</code>線程實例化一個<code>ThreadLocal</code>對象稚配,查看其維護(hù)的<code>threadLocalHashCode</code>字段值畅涂。
public static void main(String[] args) {
    ThreadLocal<String> valueContainer = new ThreadLocal<>();
    try {
        Class<?> threadLocalClz = Class.forName("java.lang.ThreadLocal");
        Field localHashFiled = threadLocalClz.getDeclaredField("threadLocalHashCode");
        localHashFiled.setAccessible(true);
        int a = (int)(localHashFiled.get(valueContainer));
        Log.i(TAG, "hash code: " + a);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

// 結(jié)果:[TAG] hash code: 1253254570
  • 繼續(xù)設(shè)計實驗,測試得到:第7次創(chuàng)建<code>ThreadLocal</code>對象時药有,得到這個值毅戈。說明每個<code>main</code>方法中用戶自定義邏輯執(zhí)行之前苹丸,已經(jīng)創(chuàng)建好了<code>ThradLocalMap</code>愤惰。這里測不準(zhǔn)到底創(chuàng)建了多少個<code>ThreadLocal</code>變量。
public class Custom {
    private static final String TAG = "Custom";
    public static void main(String[] args) {
        for (int i = 0; i < 15; i++) {
            int hashCOde = new ValueContainer().get();
            Log.i(TAG, i + ": " + hashCOde);
        }
    }
}

class ValueContainer{
    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);
    }
    public int get(){
        return threadLocalHashCode;
    }
}
  • 通過后面對<code>Thread.currentThread()</code>的測試赘理,使用<code>new Thread().start();</code>新建立線程時得到和<code>main</code>不一樣的結(jié)論宦言。在新線程中,<code>map</code>并不會初始化商模。
A1:打印結(jié)果
反復(fù) hashCode & 15
0 7 14 5 12 3 10 1 8 15 6 13 4 11 2 9

point value
0     null 
7     class java.lang.ref.SoftReference
14    class java.lang.ref.SoftReference
5     class [Ljava.lang.Object
12    null
3     null
10    custom use

2. 進(jìn)一步對<code>Thread.currentThread()</code>進(jìn)行反射操作的測試奠旺。

Q2:Q1所測試得到的結(jié)論是否能夠進(jìn)一步被證實?
T2:設(shè)計實驗施流,通過反射响疚,拿到<code>ThraedLocal.ThreadLocalMap.Entry</code>實例,然后通過哈希值取出value瞪醋。
public static void main(String[] args) {
    ThreadLocal<String> valueContainer0 = new ThreadLocal<String>();
    valueContainer0.set("hello world");
    ThreadLocal<String> valueContainer = new ThreadLocal<String>();
    valueContainer.set("hello");
 
    try {
        Class<?> threadClz = Class.forName("java.lang.Thread");
        Field mapFiled = threadClz.getDeclaredField("threadLocals");
        mapFiled.setAccessible(true);
  
         Object maps = mapFiled.get(Thread.currentThread());

//            Log.i(TAG, "class type: " + maps.getClass());
   
        Class<?> threadLocalMapClz = Class.forName("java.lang.ThreadLocal$ThreadLocalMap");
        Field tableFiled = threadLocalMapClz.getDeclaredField("table");
        tableFiled.setAccessible(true);
   
        Object[] table = (Object[])tableFiled.get(maps);
    
        Class<?> threadLocalMapEntryClz = Class.forName("java.lang.ThreadLocal$ThreadLocalMap$Entry");
        Field valueFiled = threadLocalMapEntryClz.getDeclaredField("value");
        valueFiled.setAccessible(true);


        for (int i = 0; i < table.length; i++) {
            // Log.i(TAG, i + ": " + table[i]);
            if (table[i] != null) {
                Object value = valueFiled.get(table[i]);
                Log.i(TAG, "type: " + value.getClass());
                Log.i(TAG, i + " value: " + value.toString());
                System.out.println();
            }
        }

        Class<?> threadLocalClz = Class.forName("java.lang.ThreadLocal");
        Field localHashFiled = threadLocalClz.getDeclaredField("threadLocalHashCode");
        localHashFiled.setAccessible(true);
        int hashCode = (int)(localHashFiled.get(valueContainer));
        Log.i(TAG, "hash code: " + hashCode);
    
        int i = hashCode & (table.length - 1);
    
        Log.i(TAG, "i =: " + i);
    } catch (Exception e) {
        e.printStackTrace();
    }    
}
A2:打印結(jié)果
[TAG] type: class java.lang.String
[TAG] 1 value: hello

[TAG] type: class [Ljava.lang.Object;
[TAG] 5 value: [Ljava.lang.Object;@7ea987ac

[TAG] type: class java.lang.ref.SoftReference
[TAG] 7 value: java.lang.ref.SoftReference@12a3a380

[TAG] type: class java.lang.String
[TAG] 10 value: hello world

[TAG] type: class java.lang.ref.SoftReference
[TAG] 14 value: java.lang.ref.SoftReference@29453f44

[TAG] hash code: -1401181199
[TAG] i =: 1

3. 對Q1的補充

Q3:當(dāng)使用<code>new Thread().start();</code>時忿晕,如果新建立一個<code>ThreadLocal</code>,此時的map是什么樣子的银受?
T3:實驗代碼
public class Sample_ThreadLocal {

    private static final String TAG = "TAG";

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                ThreadLocal<String> valueContainer = new ThreadLocal<String>();
                valueContainer.set("hello");
                // 反射代碼
            }
        }).start();
    }
}
A3:打印結(jié)果
[TAG] current size: 1
[TAG] current threshold: 10
[TAG] current table.length: 16
[TAG] type: class java.lang.String
[TAG] 10 value: hello

[TAG] hash code: 1253254570
[TAG] i =: 10

三践盼、線程安全的<code>AtomicInteger</code>

1. ABA問題鸦采。

CAS下ABA問題及優(yōu)化方案

CAS樂觀鎖機制確實能夠提升吞吐,并保證一致性咕幻,但在極端情況下可能會出現(xiàn)ABA問題渔伯。

(1). 場景一:庫存數(shù)量
  • 并發(fā)1(上):獲取出數(shù)據(jù)的初始值是A,后續(xù)計劃實施CAS樂觀鎖肄程,期望數(shù)據(jù)仍是A的時候锣吼,修改才能成功
  • 并發(fā)2:將數(shù)據(jù)修改成B
  • 并發(fā)3:將數(shù)據(jù)修改回A
  • 并發(fā)1(下):CAS樂觀鎖,檢測發(fā)現(xiàn)初始值還是A蓝厌,進(jìn)行數(shù)據(jù)修改

上述并發(fā)環(huán)境下吐限,并發(fā)1在修改數(shù)據(jù)時,雖然還是A褂始,但已經(jīng)不是初始條件的A了诸典。中間發(fā)生了A變B,B又變A的變化崎苗,此A已經(jīng)非彼A狐粱,數(shù)據(jù)卻成功修改,可能導(dǎo)致錯誤胆数,這就是CAS引發(fā)的所謂的ABA問題肌蜻。

(2). 場景二:堆棧實現(xiàn)

有如下一個堆棧,

某堆棧(上面為頂)
  • 并發(fā)1(上):獲取出數(shù)據(jù)的初始值是A必尼,后續(xù)計劃實施CAS樂觀鎖蒋搜,期望數(shù)據(jù)仍是A的時候,修改才能成功
  • 并發(fā)2:將A出棧
  • 并發(fā)3:將B出棧
  • 并發(fā)1(下):CAS樂觀鎖判莉,檢測發(fā)現(xiàn)初始值還是A豆挽,進(jìn)行數(shù)據(jù)修改
(3). 分析

ABA問題導(dǎo)致的原因,是CAS過程中只簡單進(jìn)行了“值”的校驗券盅,再有些情況下帮哈,“值”相同不會引入錯誤的業(yè)務(wù)邏輯(例如庫存),有些情況下锰镀,“值”雖然相同娘侍,卻已經(jīng)不是原來的數(shù)據(jù)了。

(4). Java中的解決方案

ABA問題產(chǎn)生及解決方案

  • <code>AtomicStampedReference</code>:內(nèi)部維護(hù)了對象值和版本號泳炉,在創(chuàng)建<code>AtomicStampedReference</code>對象時憾筏,需要傳入初始值和初始版本號, 當(dāng)<code>AtomicStampedReference</code>設(shè)置對象值時花鹅,對象值以及狀態(tài)戳都必須滿足期望值氧腰,寫入才會成功。
  • <code>AtomicMarkableReference </code>:<code>AtomicStampedReference</code>可以給引用加上版本號,追蹤引用的整個變化過程容贝,如:A -> B -> C -> D - > A自脯,通過<code>AtomicStampedReference</code>,我們可以知道斤富,引用變量中途被更改了3次 但是膏潮,有時候,我們并不關(guān)心引用變量更改了幾次满力,只是單純的關(guān)心是否更改過焕参,所以就有了<code>AtomicMarkableReference </code>,<code>AtomicMarkableReference </code>的唯一區(qū)別就是不再用int標(biāo)識引用油额,而是使用boolean變量——表示引用變量是否被更改過叠纷。

2. CAS在<code>AtomicInteger</code>中的應(yīng)用

AtomicInteger.class:

private static final Unsafe unsafe = Unsafe.getUnsafe();

private static final long valueOffset;

static {
    try {
        valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

public final int getAndAdd(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta);
}

Unsafe.class:
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

其中<code>compareAndSwapInt</code>詳解:

public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
// 此方法是Java的native方法,并不由Java語言實現(xiàn)潦嘶。
// 方法的作用是涩嚣,讀取傳入對象o在內(nèi)存中偏移量為offset位置的值與期望值expected作比較。
// 相等就把x值賦值給offset位置的值掂僵。方法返回true航厚。
// 不相等,就取消賦值锰蓬,方法返回false幔睬。

四、源碼解讀

1. 場景一:一個新的線程使用<code>ThreadLocal</code>的<code>set</code>和<code>get</code>方法

void main(){
    // 1. 使用 無參的構(gòu)造方法初始化ThreadLocal對象芹扭。
    ThreadLocal<String> threadLocal = new ThreadLocal<>();
    threadLocal.set("hello world!");
}
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        // 2. 上面的可以不看麻顶,直接走到了這里 T:當(dāng)前線程 V:值
        createMap(t, value);
}

void createMap(Thread t, T firstValue) {
    // 3. 在createMap方法中給當(dāng)前線程的map賦了初值
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap.class{
    private static final int INITIAL_CAPACITY = 16;
    private Entry[] table;
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        // 4. 新建了一個數(shù)組map。長度是固定值 = 16
        table = new Entry[INITIAL_CAPACITY];
        // 5. 計算 當(dāng)前l(fā)ocal的哈希值 與上 0...0 1111 1111
        // 5.2疑問:初始化操作只會在這里舱卡,所以i = 0
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }
}

Thread.class{
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocal.class{
    // 5.1 local的哈希值賦初值
    private final int threadLocalHashCode = nextHashCode();
    private static AtomicInteger nextHashCode = new AtomicInteger();
    private static final int HASH_INCREMENT = 0x61c88647;
    private static int nextHashCode() {
        // 5.2 疑問:閱讀源碼可知辅肾,getAndAdd返回的值是AtomicInteger原來的值,那么初始化的時候就是返回0灼狰;
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
}

// 6. get方法的初始化使用和上述過程類似宛瞄。不再贅述浮禾。

2. 場景二:一個一個線程已經(jīng)擁有了一個map實例對象并使用local存儲了value

(1). <code>get</code>交胚。
ThreadLocal.class {
    public T get() {
        Thread t = Thread.currentThread();
        
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 1. 此時會使用map的getEntry來獲取value
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 如果是初始化的情況則會直接返回通過重寫initValue時回調(diào)的value
        return setInitialValue();
    }
}

ThreadLocalMap.class {
    private Entry getEntry(ThreadLocal<?> key) {
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        if (e != null && e.get() == key)
            return e;
        else
            return getEntryAfterMiss(key, i, e);
    }
}

上述代碼不難理解。此時我們考慮當(dāng)一個線程已經(jīng)初始化了map后盈电,如何<code>set</code>和<code>get</code>

(2). <code>set</code>蝴簇。
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 1. 此時會調(diào)用map的set方法。
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocalMap.class{
    private void set(ThreadLocal<?> key, Object value) {
        Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);
        // 2.1 新建的時候 e == null匆帚。
        for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
     
            if (k == key) {
                e.value = value;
                return;
            }
            // 2.2 當(dāng)ThreadLocal被回收了怎么辦熬词?
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        // 3. 新建一個Entry用來存儲value
        tab[i] = new Entry(key, value); 
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            // 5. 擴容處理
            rehash();
    }
    
    // 參數(shù)
    // i:最新添加的Entry的下標(biāo)
    // n:當(dāng)前存儲了多少個Entry
    private boolean cleanSomeSlots(int i, int n) {
        boolean removed = false;
        Entry[] tab = table;
        int len = tab.length;
        do {
            i = nextIndex(i, len);
            Entry e = tab[i];
            // 4. e.get()是創(chuàng)建Entry傳遞的ThreadLocal
            if (e != null && e.get() == null) {
                // 4.1 當(dāng)ThreadLocal被回收了
                n = len;
                removed = true;
                // 4.2 釋放老舊的Entry
                i = expungeStaleEntry(i);
            }
        } while ( (n >>>= 1) != 0);
        
        return removed;
    }
}

五、其他細(xì)節(jié)

考慮到<code>Entry</code>繼承了<code>WeakReference</code>,關(guān)于它以及它父類的實現(xiàn)互拾,還有很多的細(xì)節(jié)需要注意歪今。

  • 每次<code>set</code>之后都會檢查清除stale的<code>Entry</code>,并監(jiān)測是否需要觸發(fā)擴容颜矿。
  • 當(dāng)<code>ThreadLocalMap.size >= threshold</code>時寄猩,hash表就會觸發(fā)擴容。
  • 神奇的魔數(shù)<code>1640531527</code>骑疆,還需要再算一下數(shù)學(xué)原理田篇。(連續(xù)生成的哈希碼之間的差異(增量值),將隱式順序線程本地id轉(zhuǎn)換為幾乎最佳分布的乘法哈希值箍铭,這些不同的哈希值最終生成一個2的冪次方的哈希表泊柬。)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市诈火,隨后出現(xiàn)的幾起案子兽赁,更是在濱河造成了極大的恐慌,老刑警劉巖冷守,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件闸氮,死亡現(xiàn)場離奇詭異,居然都是意外死亡教沾,警方通過查閱死者的電腦和手機蒲跨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來授翻,“玉大人或悲,你說我怎么就攤上這事】疤疲” “怎么了巡语?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長淮菠。 經(jīng)常有香客問我男公,道長,這世上最難降的妖魔是什么合陵? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任枢赔,我火速辦了婚禮,結(jié)果婚禮上拥知,老公的妹妹穿的比我還像新娘踏拜。我一直安慰自己,他們只是感情好低剔,可當(dāng)我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布速梗。 她就那樣靜靜地躺著肮塞,像睡著了一般。 火紅的嫁衣襯著肌膚如雪姻锁。 梳的紋絲不亂的頭發(fā)上枕赵,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天,我揣著相機與錄音位隶,去河邊找鬼烁设。 笑死,一個胖子當(dāng)著我的面吹牛钓试,可吹牛的內(nèi)容都是我干的装黑。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼弓熏,長吁一口氣:“原來是場噩夢啊……” “哼恋谭!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起挽鞠,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤疚颊,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后信认,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體材义,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年嫁赏,在試婚紗的時候發(fā)現(xiàn)自己被綠了其掂。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡潦蝇,死狀恐怖款熬,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情攘乒,我是刑警寧澤贤牛,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站则酝,受9級特大地震影響殉簸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜沽讹,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一般卑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧妥泉,春花似錦椭微、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至刽沾,卻和暖如春本慕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背侧漓。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工锅尘, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人布蔗。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓藤违,卻偏偏與公主長得像,于是被迫代替她去往敵國和親纵揍。 傳聞我的和親對象是個殘疾皇子顿乒,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,724評論 2 354