
一種解決多線程環境下成員變量的問題的方案,但是與線程同步無關,其思路是為每一個線程創建一個單獨的變量副本,從而每個線程都可以獨立地改變所擁有的變量副本,而不會影響其他線程所對應的副本;
ThreadLocal不是用于解決共享變量的問題的,也不是為了協調線程同步而存在,而是為了方便每個線程處理自己的狀態而引入的一個機制;
1、threadlocal使用
void set(Object value)
設置當前線程的線程局部變量的值
public Object get()
該方法返回當前線程所對應的線程局部變量
public void remove()
將當前線程局部變量的值刪除,目的是為了減少內存的占用,該方法是JDK 5.0新增的方法。需要指出的是,當線程結束后,對應該線程的局部變量將自動被垃圾回收,所以顯式調用該方法清除線程的局部變量并不是必須的操作,但它可以加快內存回收的速度;
創建一個ThreadLocal對象
private ThreadLocal<Integer> localInt = new ThreadLocal<>();
public int setAndGet(){
localInt.set(8);
return localInt.get();
}
設置變量的值為8
ThreadLocal里設置的值,只有當前線程自己看得見,這意味著你不可能通過其他線程為它初始化值。為了彌補這一點,ThreadLocal提供了一個withInitial()方法統一初始化所有線程的ThreadLocal的值:
private ThreadLocal<Integer> localInt = ThreadLocal.withInitial(() -> 6);
上述代碼將ThreadLocal的初始值設置為6,這對全體線程都是可見的
2、ThreadLocal源碼分析
ThreadLocal類源碼
/**
* ThreadLocals依賴于附加到每個線程的每個線程線性探測哈希映射(thread.ThreadLocals和可繼承的ThreadLocal)。
* ThreadLocal對象充當鍵,通過threadLocalHashCode進行搜索。
* 這是一個自定義哈希代碼(僅在ThreadLocalMaps中有用),在相同線程使用連續構造的ThreadLocal的常見情況下消除了沖突,
* 而在不常見的情況下保持良好的行為。
*/
private final int threadLocalHashCode = nextHashCode();
/**
* 要給出的下一個哈希代碼。原子更新。從零開始。
*/
private static AtomicInteger nextHashCode = new AtomicInteger();
/**
* 連續生成的哈希碼之間的差異-將隱式順序線程本地ID轉換為兩個大小表的冪的近似最優的乘法哈希值。
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* 返回下一個哈希代碼。
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
/**
* 設置調整大小閾值,在最壞的情況下為 2/3 負載系數
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* 根據傳入的下標,返回下一個下標 (環形: 0-1-...-(len-1)-len-0-1-...-len)
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* ThreadLocalMap 內部類
*/
static class ThreadLocalMap {
private Entry[] table;//數據數組
private int size = 0;//數組大小
private int threshold; //閾值
private static final int INITIAL_CAPACITY = 16; //默認大小
/*
* Entry 繼承WeakReference,并且用ThreadLocal作為key.
* 如果key為null(entry.get() == null),意味著key不再被引用,
* 因此這時候entry也可以從table中清除。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value; //存儲線程值
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* 初始化
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}
作為一個存儲數據的類,關鍵點就在get和set方法。
(1)set 方法
//set 方法
public void set(T value) {
//獲取當前線程
Thread t = Thread.currentThread();
//實際存儲的數據結構類型
ThreadLocalMap map = getMap(t);
//如果存在map就直接set,沒有則創建map并set
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
//getMap方法
ThreadLocalMap getMap(Thread t) {
//thred中維護了一個ThreadLocalMap
return t.threadLocals;
}
//createMap
void createMap(Thread t, T firstValue) {
//實例化一個新的ThreadLocalMap,并賦值給線程的成員變量threadLocals
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
- 首先獲取當前線程,并根據當前線程獲取一個Map。
- 如果獲取的Map不為空,則將參數設置到Map中(當前ThreadLocal的引用作為key)。
- (這里調用了ThreadLocalMap的set方法)**。
- 如果Map為空,則給該線程創建 Map,并設置初始值。
- (這里調用了ThreadLocalMap的構造方法)**。
- 構造方法`ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)。
/*
* firstKey : 本ThreadLocal實例(this)
* firstValue : 要保存的線程本地變量
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//初始化table
table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
//計算索引(重點代碼)
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//設置值
table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
size = 1;
//設置閾值
setThreshold(INITIAL_CAPACITY);
}
構造函數首先創建一個長度為16的Entry數組,然后計算出firstKey對應的索引,然后存儲到table中,并設置size和threshold。
ThreadLocalMap中的set方法
private void set(ThreadLocal<?> key, Object value) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
//計算索引(重點代碼,剛才分析過了)
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();
//ThreadLocal 對應的 key 存在,直接覆蓋之前的值
if (k == key) {
e.value = value;
return;
}
// key為 null,但是值不為 null,說明之前的 ThreadLocal 對象已經被回收了,
// 當前數組中的 Entry 是一個陳舊(stale)的元素
if (k == null) {
//用新元素替換陳舊的元素,這個方法進行了不少的垃圾清理動作,防止內存泄漏
replaceStaleEntry(key, value, i);
return;
}
}
//ThreadLocal對應的key不存在并且沒有找到陳舊的元素,則在空元素的位置創建一個新的Entry。
tab[i] = new Entry(key, value);
int sz = ++size;
/**
* cleanSomeSlots用于清除那些e.get()==null的元素,
* 這種數據key關聯的對象已經被回收,所以這個Entry(table[index])可以被置null。
* 如果沒有清除任何entry,并且當前使用量達到了負載因子所定義(長度的2/3),那么進行 * rehash(執行一次全表的掃描清理工作)
*/
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
/**
* 獲取環形數組的下一個索引
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
- 首先還是根據key計算出索引 i,然后查找i位置上的Entry。
- 若是Entry已經存在并且key等于傳入的key,那么這時候直接給這個Entry賦新的value值。
- 若是Entry存在,但是key為null,則調用replaceStaleEntry來更換這個key為空的Entry。
- 不斷循環檢測,直到遇到為null的地方,這時候要是還沒在循環過程中return,那么就在這個null的位置新建一個Entry,并且插入,同時size增加1。
- ThreadLocalMap使用線性探測法來解決哈希沖突的;該方法一次探測下一個地址,直到有空的地址后插入,若整個空間都找不到空余的地址,則產生溢出。
(2)get()方法
//ThreadLocal中get方法
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();
}
//ThreadLocalMap中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);
}
通過計算出索引直接從數組對應位置讀取即可;
(3)ThreadLocal特性
- ThreadLocal和Synchronized都是為了解決多線程中相同變量的訪問沖突問題;
- Synchronized是通過線程等待,犧牲時間來解決訪問沖突
- ThreadLocal是通過每個線程單獨一份存儲空間,犧牲空間來解決沖突,并且相比于Synchronized,ThreadLocal具有線程隔離的效果,只有在線程內才能獲取到對應的值,線程外則不能訪問到想要的值;
- 正因為ThreadLocal的線程隔離特性,使他的應用場景相對來說更為特殊一些。在android中Looper、ActivityThread以及AMS中都用到了ThreadLocal;
- 當某些數據是以線程為作用域并且不同線程具有不同的數據副本的時候,就可以考慮采用ThreadLocal;
3、ThreadLocal內存泄漏
我們調用threadLocal的set,get方法時,會判斷當前的key是否為null,將Entry中的value賦值為null,但是這個釋放value還有其他條件限制,并不是一定會發生,當系統內存不足時,由于Entry中的key繼承軟引用,回被垃圾回收器回收調,這時,Entry中的key為null,無法被線程訪問,但是value仍然占用一定的內存空間,雖然在調用set,get方法時有可能進行系統回收,仍然無法回收無用所有內存。無法被訪問的vlaue就會導致內存泄漏,怎么解決內存泄漏呢,最好的方法就是當我們使用完變量副本后及時調用remove方法,手動進行垃圾回收。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
/**
* Remove the entry for key.
*/
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)]) {
if (e.get() == key) {
e.clear();//清除value
expungeStaleEntry(i);
return;
}
}
}
- 當線程發生內存泄漏時,線程與內部的ThreadLocalMap之間存在著強引用,導致ThreadLocalMap無法被釋放,這時由于ThreadLocalMap中的Entry的key為弱引用,ThreadLocal容易被回收,導致key為null,當調用remove方法時,會清除key為null對應的value。
- 所以為了避免內存泄漏的出現,我們在使用完ThreadLocal的set方法后,及時調用remove方法進行內存釋放。避免出現內存泄漏。
