更多 Java 并發(fā)編程方面的文章蚯妇,請參見文集《Java 并發(fā)編程》
Thread Local 線程本地變量
在每個線程中都創(chuàng)建一個變量的副本省容,線程內(nèi)共享封锉,線程間互斥铆遭。
ThreadLocal 是一個為線程提供線程局部變量的工具類磕瓷。為線程提供一個線程私有的變量副本盒齿,這樣多個線程都可以隨意更改自己線程局部的變量,不會影響到其他線程困食。
不過需要注意的是边翁,ThreadLocal 提供的只是一個淺拷貝,如果變量是一個引用類型硕盹,那么就要考慮它內(nèi)部的狀態(tài)是否會被改變符匾,想要解決這個問題可以通過重寫 ThreadLocal 的 initialValue()
函數(shù)來自己實現(xiàn)深拷貝,建議在使用 ThreadLocal 時一開始就重寫該函數(shù)瘩例。
ThreadLocal 與像 synchronized 這樣的鎖機制是不同的:
- 鎖更強調(diào)的是如何同步多個線程去正確地共享一個變量啊胶,ThreadLocal 則是為了解決同一個變量如何不被多個線程共享
- 從性能開銷的角度上來講,如果鎖機制是用時間換空間的話垛贤,那么 ThreadLocal 就是用空間換時間
ThreadLocal 中含有一個叫做 ThreadLocalMap 的內(nèi)部類焰坪,該類為一個采用線性探測法實現(xiàn)的 HashMap。
它的 key 為 ThreadLocal 對象而且還使用了 WeakReference聘惦,ThreadLocalMap 正是用來存儲變量副本的琳彩。
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
ThreadLocal 中只含有三個成員變量,這三個變量都是與 ThreadLocalMap 的 hash 策略相關的部凑。
public class ThreadLocal<T> {
/**
* ThreadLocals rely on per-thread linear-probe hash maps attached
* to each thread (Thread.threadLocals and
* inheritableThreadLocals). The ThreadLocal objects act as keys,
* searched via threadLocalHashCode. This is a custom hash code
* (useful only within ThreadLocalMaps) that eliminates collisions
* in the common case where consecutively constructed ThreadLocals
* are used by the same threads, while remaining well-behaved in
* less common cases.
*/
private final int threadLocalHashCode = nextHashCode();
/**
* The next hash code to be given out. Updated atomically. Starts at
* zero.
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
唯一的實例變量 threadLocalHashCode
是用來進行尋址的 hashcode,它由函數(shù) nextHashCode()
生成碧浊,該函數(shù)簡單地通過一個增量 HASH_INCREMENT
來生成 hashcode涂邀。至于為什么這個增量為 0x61c88647,主要是因為 ThreadLocalMap 的初始大小為16箱锐,每次擴容都會為原來的2倍比勉,這樣它的容量永遠為2的n次方,該增量選為 0x61c88647 也是為了盡可能均勻地分布驹止,減少碰撞沖突浩聋。
獲取當前線程中該變量的值 - get()
要獲得當前線程私有的變量副本需要調(diào)用 get()
函數(shù)。首先臊恋,它會調(diào)用 getMap()
函數(shù)去獲得當前線程的ThreadLocalMap衣洁,這個函數(shù)需要接收當前線程的實例作為參數(shù)。如果得到的 ThreadLocalMap 為 null抖仅,那么就去調(diào)用 setInitialValue()
函數(shù)來進行初始化坊夫,如果不為 null砖第,就通過 map 來獲得變量副本并返回。
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();
}
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
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()
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
移除當前線程中該變量的值 - remove()
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
getMap()
函數(shù)與 createMap()
函數(shù)的實現(xiàn)也十分簡單环凿,但是通過觀察這兩個函數(shù)可以發(fā)現(xiàn)一個秘密:ThreadLocalMap 是存放在 Thread 中的梧兼。
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
Thread
類中包括:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
仔細想想其實就能夠理解這種設計的思想。有一種普遍的方法是通過一個全局的線程安全的 Map 來存儲各個線程的變量副本智听,但是這種做法已經(jīng)完全違背了 ThreadLocal 的本意羽杰,設計 ThreadLocal 的初衷就是為了避免多個線程去并發(fā)訪問同一個對象,盡管它是線程安全的到推。
而在每個 Thread 中存放與它關聯(lián)的 ThreadLocalMap 是完全符合 ThreadLocal 的思想的考赛,當想要對線程局部變量進行操作時,只需要把 Thread 作為 key 來獲得 Thread 中的 ThreadLocalMap 即可环肘。這種設計相比采用一個全局 Map 的方法會多占用很多內(nèi)存空間欲虚,但也因此不需要額外的采取鎖等線程同步方法而節(jié)省了時間上的消耗。
使用示例
提供的方法:
-
T initialValue()
:設置初始值 -
public T get()
:獲取當前線程中該變量的值 -
public void set(T value)
:設置當前線程中該變量的值 -
public void remove()
:移除當前線程中該變量的值
示例:
我們希望每一個線程維護一個 Unique ID悔雹。
public class ThreadLocal_Test {
private static final AtomicInteger nextId = new AtomicInteger(0);
public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new MyThread().start();
}
}
static class MyThread extends Thread {
public void run() {
System.out.println(Thread.currentThread().getName() + " Unique ID: " + threadLocal.get());
}
}
}
輸出如下:
Thread-0 Unique ID: 0
Thread-1 Unique ID: 1
Thread-2 Unique ID: 2
Thread-3 Unique ID: 3
Thread-4 Unique ID: 4
ThreadLocal 中的內(nèi)存泄漏
如果 ThreadLocal 被設置為 null 后复哆,而且沒有任何強引用指向它,根據(jù)垃圾回收的可達性分析算法腌零,ThreadLocal 將會被回收梯找。這樣一來,ThreadLocalMap 中就會含有 key 為 null 的 Entry益涧,而且ThreadLocalMap 是在 Thread 中的锈锤,只要線程遲遲不結(jié)束,這些無法訪問到的 value 會形成內(nèi)存泄漏闲询。為了解決這個問題久免,ThreadLocalMap 中的 getEntry()、set() 和 remove() 函數(shù)都會清理 key 為 null 的 Entry扭弧。
在上文中我們發(fā)現(xiàn)了 ThreadLocalMap 的 key 是一個弱引用阎姥,那么為什么使用弱引用呢?使用強引用key與弱引用key的差別如下:
- 強引用 key:ThreadLocal 被設置為 null鸽捻,由于 ThreadLocalMap 持有 ThreadLocal 的強引用呼巴,如果不手動刪除,那么 ThreadLocal 將不會回收御蒲,產(chǎn)生內(nèi)存泄漏衣赶。
- 弱引用 key:ThreadLocal 被設置為 null,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用厚满,即便不手動刪除府瞄,ThreadLocal 仍會被回收,ThreadLocalMap 在之后調(diào)用 set()碘箍、getEntry() 和 remove() 函數(shù)時會清除所有 key 為 null 的 Entry摘能。
但要注意的是续崖,ThreadLocalMap僅僅含有這些被動措施來補救內(nèi)存泄漏問題。如果你在之后沒有調(diào)用ThreadLocalMap 的 set()团搞、getEntry() 和 remove() 函數(shù)的話严望,那么仍然會存在內(nèi)存泄漏問題。
在使用線程池的情況下逻恐,如果不及時進行清理像吻,內(nèi)存泄漏問題事小,甚至還會產(chǎn)生程序邏輯上的問題复隆。所以拨匆,為了安全地使用 ThreadLocal,必須要像每次使用完鎖就解鎖一樣挽拂,在每次使用完 ThreadLocal 后都要調(diào)用 remove() 來清理無用的Entry惭每。