【搞定Java併發程式設計】第6篇:ThreadLocal詳解
上一篇:synchronized關鍵字:https://blog.csdn.net/pcwl1206/article/details/84849400
目 錄:
1、ThreadLocal是什麼?
ThreadLocal是啥?以前面試別人時就喜歡問這個,有些夥伴喜歡把它和執行緒同步機制混為一談,事實上ThreadLocal與執行緒同步無關。ThreadLocal雖然提供了一種解決多執行緒環境下成員變數的問題,但是它並不是解決多執行緒共享變數的問題。那麼ThreadLocal到底是什麼呢?
API是這樣介紹它的:
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g.,a user ID or Transaction ID).
翻譯過來就是:
該類提供了執行緒區域性 (thread-local) 變數。這些變數不同於它們的普通對應物,因為訪問某個變數(通過其
get
或set
方法)的每個執行緒都有自己的區域性變數,它獨立於變數的初始化副本。ThreadLocal
例項通常是類中的 private static 欄位,它們希望將狀態與某一個執行緒(例如,使用者 ID 或事務 ID)相關聯。
所以ThreadLocal與執行緒同步機制不同,執行緒同步機制是多個執行緒共享同一個變數,而ThreadLocal是為每一個執行緒建立一個單獨的變數副本,故而每個執行緒都可以獨立地改變自己所擁有的變數副本,而不會影響其他執行緒所對應的副本
ThreadLocal定義了四個方法:
1、get():返回此執行緒區域性變數的當前執行緒副本中的值;
2、initialValue():返回此執行緒區域性變數的當前執行緒的“初始值”;
3、remove():移除此執行緒區域性變數中當前執行緒的值;
4、set(T value):將此執行緒區域性變數的當前執行緒副本中的值設定為指定值。
除了這四個方法,ThreadLocal內部還有一個靜態內部類ThreadLocalMap,該內部類才是實現執行緒隔離機制的關鍵,get()、set()、remove()都是基於該內部類操作。ThreadLocalMap提供了一種用鍵值對方式儲存每一個執行緒的變數副本的方法,key為當前ThreadLocal物件,value則是對應執行緒的變數副本。
對於ThreadLocal需要注意的有兩點:
1. ThreadLocal例項本身是不儲存值,它只是提供了一個在當前執行緒中找到副本值的key;
2. 是ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal中,有些小夥伴會弄錯他們的關係。
下圖是Thread、ThreadLocal、ThreadLocalMap的關係:
2、ThreadLocal使用示例
public class SeqCount {
private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>(){
// 實現initialValue
public Integer initialValue(){
return 0;
}
};
public int nextSeq(){
seqCount.set(seqCount.get() + 1);
return seqCount.get();
}
public static void main(String[] args) {
SeqCount seqCount = new SeqCount();
SeqThread thread1 = new SeqThread(seqCount);
SeqThread thread2 = new SeqThread(seqCount);
SeqThread thread3 = new SeqThread(seqCount);
SeqThread thread4 = new SeqThread(seqCount);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
public class SeqThread extends Thread {
private SeqCount seqCount;
SeqThread(SeqCount seqCount) {
this.seqCount = seqCount;
}
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + " seqCount :" + seqCount.nextSeq());
}
}
}
執行結果:
從執行結果可以看出,ThreadLocal確實是可以達到執行緒隔離機制,確保變數的安全性。這裡我們想一個問題,在上面的程式碼中ThreadLocal的initialValue()方法返回的是0,假如該方法返回的是一個物件呢,會產生什麼後果呢?例如:
A a = new A();
private static ThreadLocal<A> seqCount = new ThreadLocal<A>(){
// 實現initialValue()
public A initialValue() {
return a;
}
};
class A{
// ....
}
- 具體過程請參考:對ThreadLocal實現原理的一點思考
3、ThreadLocal原始碼分析
ThreadLocal雖然解決了這個多執行緒變數的複雜問題,但是它的原始碼實現卻是比較簡單的。ThreadLocalMap是實現ThreadLocal的關鍵,我們先從它入手。
3.1、ThreadLocalMap
ThreadLocalMap是ThreadLocal中的一個靜態內部類。ThreadLocalMap其內部利用Entry來實現key-value的儲存,如下:
public class ThreadLocal<T> {
// ... 其他方法
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;
}
}
// ...其他方法
}
// ...其他方法
}
從上面程式碼中可以看出Entry的key就是ThreadLocal,而value就是值。同時,Entry也繼承WeakReference,所以說Entry所對應key(ThreadLocal例項)的引用為一個弱引用(關於弱引用這裡就不多說了,感興趣的可以關注這篇部落格:Java 理論與實踐: 用弱引用堵住記憶體洩漏)。
ThreadLocalMap的原始碼稍微多了點,我們就看兩個最核心的方法getEntry()、set(ThreadLocal> key, Object value)方法。
3.1.1、getEntry()
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);
}
由於採用了開放定址法,所以當前key的雜湊值和元素在陣列的索引並不是完全對應的,首先取一個探測數(key的雜湊值),如果所對應的key就是我們所要找的元素,則返回,否則呼叫getEntryAfterMiss(),如下:
private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
這裡有一個重要的地方,當key == null時,呼叫了expungeStaleEntry()方法,該方法用於處理key == null,有利於GC回收,能夠有效地避免記憶體洩漏。
3.1.2、set(ThreadLocal> key, Object value)
private void set(ThreadLocal<?> key, Object value) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
// 根據 ThreadLocal 的雜湊值,查詢對應元素在陣列中的位置
int i = key.threadLocalHashCode & (len - 1);
// 採用“線性探測法”,尋找合適位置
for (ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// key 存在,直接覆蓋
if (k == key) {
e.value = value;
return;
}
// key == null,但是存在值(因為此處的e != null),說明之前的ThreadLocal物件已經被回收了
if (k == null) {
// 用新元素替換陳舊的元素
replaceStaleEntry(key, value, i);
return;
}
}
// ThreadLocal對應的key例項不存在也沒有陳舊元素,new 一個
tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
int sz = ++size;
// cleanSomeSlots 清除陳舊的Entry(key == null)
// 如果沒有清理陳舊的 Entry 並且陣列中的元素大於了閾值,則進行 rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
這個set()操作和我們在集合瞭解的put()方式有點兒不一樣,雖然他們都是key-value結構,不同在於他們解決雜湊衝突的方式不同。集合Map的put()採用的是拉鍊法,而ThreadLocalMap的set()則是採用開放定址法(具體請參考雜湊衝突處理系列部落格)。掌握了開放地址法該方法就一目瞭然了。
set()操作除了儲存元素外,還有一個很重要的作用,就是replaceStaleEntry()和cleanSomeSlots(),這兩個方法可以清除掉key == null 的例項,防止記憶體洩漏。在set()方法中還有一個變數很重要:threadLocalHashCode,定義如下:
private final int threadLocalHashCode = nextHashCode();
從名字上面我們可以看出threadLocalHashCode應該是ThreadLocal的雜湊值,定義為final,表示ThreadLocal一旦建立其雜湊值就已經確定了,生成過程則是呼叫nextHashCode():
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
nextHashCode表示分配下一個ThreadLocal例項的threadLocalHashCode的值,HASH_INCREMENT則表示分配兩個ThradLocal例項的threadLocalHashCode的增量,從nextHashCode就可以看出他們的定義。
上面的兩個方法都是靜態內部類ThreadLocalMap中的方法,下面讓我們看看ThreadLocal中的其他方法。
3.2、get()
返回此執行緒區域性變數的當前執行緒副本中的值。
public T get() {
// 獲取當前執行緒
Thread t = Thread.currentThread();
// 獲取當前執行緒的成員變數 threadLocal
ThreadLocalMap map = getMap(t);
if (map != null) {
// 從當前執行緒的ThreadLocalMap獲取相對應的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
// 獲取目標值
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
首先通過當前執行緒獲取所對應的成員變數ThreadLocalMap,然後通過ThreadLocalMap獲取當前ThreadLocal的Entry,最後通過所獲取的Entry獲取目標值result。
getMap()方法可以獲取當前執行緒所對應的ThreadLocalMap,如下:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
3.3、set(T value)
將此執行緒區域性變數的當前執行緒副本中的值設定為指定值。
public void set(T value) {
Thread t = Thread.currentThread(); // 獲取當前執行緒
ThreadLocalMap map = getMap(t); // 獲取當前執行緒所對應的ThreadLocalMap
if (map != null)
map.set(this, value); // ThreadLocalMap不為空時,呼叫ThreadLocalMap的set()方法
else
createMap(t, value); // ThreadLocalMap為空時,呼叫createMap()方法新建一個ThreadLocalMap
}
首先獲取當前執行緒所對應的ThreadLocalMap,如果不為空,則呼叫ThreadLocalMap的set()方法,key就是當前ThreadLocal;如果不存在,則呼叫createMap()方法新建一個ThreadLocalMap,key為當前執行緒,值為指定的value。如下:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
3.4、initialValue()
返回此執行緒區域性變數的當前執行緒的“初始值”;
protected T initialValue() {
return null;
}
該方法定義為protected級別且返回為null,很明顯是要子類實現它的,所以我們在使用ThreadLocal的時候一般都應該覆蓋該方法。該方法不能顯示呼叫,只有在第一次呼叫get()或者set()方法時才會被執行,並且僅執行1次。
3.5、remove()
將當前執行緒區域性變數的值移除。
public void remove() {
// 獲取當前執行緒的ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this); // 如果當前執行緒的ThreadLocalMap不為空,則移除
}
4、ThreadLocal為什麼會記憶體洩漏
先看這樣一個小案例:
在JAVA裡面,存在強引用、弱引用、軟引用、虛引用。這裡主要談一下強引用和弱引用。
- 強引用類似於:
A a = new A();
B b = new B();
- 現在考慮這種情況:
C c = new C(b);
b = null;
考慮下GC的情況。要知道b被置為null,那麼是否意味著一段時間後GC工作可以回收b所分配的記憶體空間呢?
答案是否定的,因為即便b被置為null,但是c仍然持有對b的引用,而且還是強引用,所以GC不會回收b原先所分配的空間!既不能回收利用,又不能使用,這就造成了記憶體洩露。
那麼如何處理呢?
可以使c = null;也可以使用弱引用!(WeakReference w = new WeakReference(b);)
下面就來看看ThreadLocal的記憶體洩露是怎麼一回事:
前面提到每個Thread都有一個ThreadLocal.ThreadLocalMap的map,該map的key為ThreadLocal例項,它為一個弱引用,我們知道弱引用有利於GC回收。當ThreadLocal的key == null時,GC就會回收這部分空間,但是value卻不一定能夠被回收,因為他還與Current Thread存在一個強引用關係,如下圖所示:
由於存在這個強引用關係,會導致value無法回收。如果這個執行緒物件不會銷燬,那麼這個強引用關係則會一直存在,就會出現記憶體洩漏情況。所以說只要這個執行緒物件能夠及時被GC回收,就不會出現記憶體洩漏。如果碰到執行緒池,那就更坑了。
那麼要怎麼避免這個問題呢?
在前面提過,在ThreadLocalMap中的setEntry()、getEntry(),如果遇到key == null的情況,會對value設定為null。當然我們也可以顯示呼叫ThreadLocal的remove()方法進行處理。
5、總結
下面對ThreadLocal進行簡單的總結:
1、ThreadLocal 不是用於解決共享變數的問題的,也不是為了協調執行緒同步而存在,而是為了方便每個執行緒處理自己的狀態而引入的一個機制。這點至關重要;
2、每個Thread內部都有一個ThreadLocal.ThreadLocalMap型別的成員變數,該成員變數用來儲存實際的ThreadLocal變數副本;
3、ThreadLocal並不是為執行緒儲存物件的副本,它僅僅只起到一個索引的作用。它的主要目的是為每一個執行緒隔離一個類的例項,這個例項的作用範圍僅限於執行緒內部。
本文轉發自:http://cmsblogs.com/?p=2442
上一篇:synchronized關鍵字:https://blog.csdn.net/pcwl1206/article/details/84849400