簡述
jdk原始碼註解中有這樣一段描述:
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).
這個類提供執行緒區域性變數。這些變數與其正常的對應方式不同,因為訪問一個的每個執行緒(通過其get或set方法)都有自己獨立初始化的變數副本。 ThreadLocal例項通常是希望將狀態與執行緒關聯的類中的私有靜態欄位(例如,使用者ID或事務ID)
需要明確的是ThreadLocal不是用於解決共享變數的問題的,也不是為了協調執行緒同步而存在,而是為了方便每個執行緒處理自己的狀態而引入的一個機制
ThreadLocal使用示例
public class SeqCount {
private static ThreadLocal seqCount = new ThreadLocal(){
// 實現initialValue()
public Integer initialValue() {
return 0;
}
};
public int nextSeq(){
seqCount.set(seqCount.get() + 1);
return seqCount.get();
}
public void remove() {
seqCount.remove();
}
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();
}
private static class SeqThread extends Thread{
private SeqCount seqCount;
SeqThread(SeqCount seqCount){
this.seqCount = seqCount;
}
public void run() {
try {
for(int i = 0 ; i < 3 ; i++){
System.out.println(Thread.currentThread().getName() + " seqCount :" + seqCount.nextSeq());
}
} finally {
seqCount.remove();
}
}
}
}
複製程式碼
執行結果:
Thread-0 seqCount :1
Thread-0 seqCount :2
Thread-0 seqCount :3
Thread-1 seqCount :1
Thread-1 seqCount :2
Thread-1 seqCount :3
Thread-3 seqCount :1
Thread-3 seqCount :2
Thread-3 seqCount :3
Thread-2 seqCount :1
Thread-2 seqCount :2
Thread-2 seqCount :3
複製程式碼
從結果可以得知,ThreadLocal確實是可以達到執行緒隔離機制,保證了變數的安全性
ThreadLocal實現原理
ThreadLocal是為每一個執行緒建立一個單獨的變數副本,所以每個執行緒都可以獨立地改變自己所擁有的變數副本,而不會影響其他執行緒所對應的副本。從其幾個方法入手
set方法
public void set(T value) {
// 獲取當前執行緒
Thread t = Thread.currentThread();
// 通過當前執行緒例項獲取ThreadLocalMap物件
ThreadLocalMap map = getMap(t);
// 若map不為null,則以當前threadLocal為鍵,value為值存放
if (map != null)
map.set(this, value);
// 若map為null,則建立ThreadLocalMap,以當前threadLocal為鍵,value為值
else
createMap(t, value);
}
複製程式碼
獲取當前執行緒例項,呼叫getMap()獲取此執行緒的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
複製程式碼
然後判斷map是否為null,若為null則還需建立threadLocalMap,以當前threadLocal為鍵,value為值存放在threadLocalMap中,若不為null直接儲存即可
get方法
public T get() {
// 獲取當前執行緒
Thread t = Thread.currentThread();
// 獲取執行緒關聯的ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 若map不為null,從map中獲取以當前threadLocal例項為key的資料
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 若map為null或者entry為null,則呼叫此方法初始化
return setInitialValue();
}
複製程式碼
get方法獲取當前執行緒關聯的ThreadLocalMap。若map不為null,以threadLocal例項為key獲取資料;若map為null或entry為null呼叫setInitialValue()方法
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
複製程式碼
與set方法差不多,但多了initialValue()方法,此方法需要子類重寫
protected T initialValue() {
return null;
}
複製程式碼
remove方法
public void remove() {
// 根據當前執行緒獲取其所關聯的ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
// 若map不為null,刪除以當前threadLocal為key的資料
if (m != null)
m.remove(this);
}
複製程式碼
ThreadLocalMap
從ThreadLocal那幾個核心方法來看,其實現都基於內部類ThreadLocalMap
ThreadLocalMap屬性
// 初始化容量
private static final int INITIAL_CAPACITY = 16;
// 雜湊表
private Entry[] table;
// 元素個數
private int size = 0;
// 擴容閾值(threshold = 底層雜湊表table的長度 len * 2 / 3)
private int threshold;
複製程式碼
內部類entry
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
複製程式碼
從原始碼中可以得知Entry的key是Threadlocal,並且Entry繼承WeakReference弱引用。注意Entry中並沒有next屬性,相對於HashMap採用鏈地址法處理衝突,ThreadLocalMap採用開放定址法
set方法
private void set(ThreadLocal key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 根據ThreadLocal的hashcode值,尋找對應Entry在陣列中的位置
int i = key.threadLocalHashCode & (len-1);
for (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肯定存在entry
// 說明之前的ThreadLocal物件已經被回收
if (k == null) {
// 替換舊entry
replaceStaleEntry(key, value, i);
return;
}
}
// 建立新entry
tab[i] = new Entry(key, value);
// 元素個數+1
int sz = ++size;
// cleanSomeSlots 清除舊Entry(key == null)
// 如果沒有要清除的資料,元素個數仍然大於閾值則擴容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
複製程式碼
每個ThreadLocal物件都有一個hash值threadLocalHashCode,每初始化一個ThreadLocal物件,hash值就增加一個固定的大小0x61c88647。在插入過程中先根據threadlocal物件的hash值,定位雜湊表的位置:
1、若此位置是空的,就建立一個Entry物件放在此位置上,呼叫cleanSomeSlots()方法清除key為null的舊entry,若沒有要清除的舊entry則判斷是否需要擴容
2、若此位置已經有Entry物件了,如果這個Entry物件的key正好是所要設定的key或key為null,則替換value值
3、若此位置Entry物件的key不符合條件,尋找雜湊表此位置+1(若到達雜湊表尾則從頭開始)
我們可以發現ThreadLocalMap採用了開放定址法來解決衝突,一旦發生了衝突,就去尋找下一個空的雜湊地址,而HashMap採用鏈地址法解決衝突在原位置利用連結串列處理
getEntry方法
private Entry getEntry(ThreadLocal key) {
// 定位
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 若此位置不為空且與entry的key返回entry物件
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
複製程式碼
理解了set,getEntry很好理解。先根據threadlocal物件的hash值,定位雜湊表的位置。若此位置entry的key和查詢的key相同的話就直接返回這個entry,若不符合呼叫getEntryAfterMiss()繼續向後找,getEntryAfterMiss方法如下:
private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal k = e.get();
// 找到和所需key相同的entry則返回
if (k == key)
return e;
// 處理key為null的entry
if (k == null)
expungeStaleEntry(i);
else
// 繼續找下一個
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
複製程式碼
remove方法
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)]) {
// 若找到所需key
if (e.get() == key) {
// 將entry的key置為null
e.clear();
// 將entry的value置為null同時entry置空
expungeStaleEntry(i);
return;
}
}
}
複製程式碼
定位在雜湊表的位置,找到相同key的entry,呼叫clear方法將key置為null,呼叫expungeStaleEntry方法刪除對應位置的過期實體,並刪除此位置後key = null的實體
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 將此位置的entry物件置空以及value置空
tab[staleSlot].value = null;
tab[staleSlot] = null;
// 元素個數-1
size--;
// Rehash until we encounter null
Entry e;
int i;
// 清除此位置後key為null的entry物件以及rehash位置不同的entry直至有位置為空為止
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
複製程式碼
記憶體洩漏
先附上四種引用與gc關係
引用型別 | 回收機制 | 用途 | 生存時間 |
強引用 | 從不回收 | 物件狀態 | JVM停止執行時 |
軟引用 | 記憶體不足時回收 | 物件快取 | 記憶體不足時終止 |
弱引用 | 物件不被引用時回收 | 物件快取 | GC後終止 |
虛引用 | 物件不被引用時回收 | 跟蹤物件的垃圾回收 | 垃圾回收後終止 |
Son son = new Son();
Parent parent = new Parent(son);
複製程式碼
當我們把son置空,由於parent持有son的引用且parent是強引用,所以gc並不回收son所分配的記憶體空間,這就導致了記憶體洩露如果是弱引用那麼上述例子,GC就會回收son所分配的記憶體空間。而ThreadLocalMap採用ThreadLocal弱引用作為key,雖然ThreadLocal是弱引用GC會回收這部分空間即key被回收,但是value卻存在一條從Current Thread過來的強引用鏈。因此只有當Current Thread銷燬時,value才能 得到釋放
那麼如何有效的避免呢?
在上述中我們可以看到ThreadLocalMap中的set/getEntry方法中,會對key為null(即ThreadLocal為null)進行判斷,如果為null的話,那麼是會對value置為null的。當然也可以通過呼叫ThreadLocal的remove方法進行釋放。
總結
ThreadLocal不是用來解決共享物件的多執行緒訪問問題,而是為了方便每個執行緒處理自己的狀態而引入的一個機制。它為每一個執行緒都提供一份變數的副本,從而實現同時訪問而互不影響。另外ThreadLocal可能存在記憶體洩漏問題,使用完ThreadLocal之後,最好呼叫remove方法
感謝
www.jianshu.com/p/377bb8408…
www.jianshu.com/p/ee8c9dccc…
cmsblogs.com/?p=2442