摘要:ThreadLocal是并發(fā)場景下用來解決變量共享問題的類轴合,它能使原本線程間共享的對象進(jìn)行線程隔離奔穿,即一個對象只對一個線程可見耘沼。但由于過度設(shè)計碌冶,比如使用弱引用和哈希碰撞庸追,導(dǎo)致理解難度大霍骄、使用成本高,反而成為故障高發(fā)點淡溯,容易出現(xiàn)內(nèi)存泄漏读整、臟數(shù)據(jù)、共享對象更新等問題咱娶。
關(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樂觀鎖機制確實能夠提升吞吐,并保證一致性咕幻,但在極端情況下可能會出現(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中的解決方案
- <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的冪次方的哈希表泊柬。)