Java-多執行緒-ThreadLocal
Java-多執行緒-ThreadLocal
0x01 摘要
本文簡單分析下ThreadLocal實現原理,再附上小例子。
0x02 ThreadLocal是什麼
ThreadLocal提供執行緒級別的私有區域性變數。這些變數和普通變數不同之處在於,通過get或set方法訪問這類變數的每個執行緒都擁有一份獨立初始化的變數副本。
ThreadLocal通常用private static
修飾,可以將狀態與該執行緒建立一對一的關係。
下面這個小例子,當第一次呼叫ThreadId.get()
時會為每個執行緒生成一個全域性唯一的、以1為步長自增的識別符號,往後都會保持不變。
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadId {
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0);
// Thread local variable containing each thread's ID
private static final ThreadLocal< Integer> threadId =
new ThreadLocal<Integer>() {
@Override protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
// Returns the current thread's unique ID, assigning it if necessary
public static int get() {
return threadId.get();
}
}
在上面的例子中,只要執行緒存活且該ThreadLocal例項可訪問,那麼每個執行緒都會擁有一個隱式引用,指向自己擁有的ThreadLocal變數副本值。
0x03 原理
3.1 ThreadLocal的屬性
3.1.1 HashCode
private final int threadLocalHashCode = nextHashCode();
// 兩個連續的hashcode差值
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private static AtomicInteger nextHashCode = new AtomicInteger();
ThreadLocals依賴於附屬於每個執行緒的線性探測雜湊對映(Thread.threadLocals和inheritableThreadLocals,都是ThreadLocalMap型別)
ThreadLocal物件充當鍵,通過threadLocalHashCode搜尋。 這是一個自定義雜湊程式碼(僅在ThreadLocalMaps中有用),它消除了在相同執行緒使用連續構造的ThreadLocals的常見情況下的衝突,同時在不太常見的情況下保持良好行為。
3.2 ThreadLocal構造方法
public ThreadLocal() {}
這裡什麼都沒做。
3.3 成員方法
3.3.1 initialValue
protected T initialValue() {
return null;
}
這個方法會在第一次使用get
方法呼叫時執行,除非在此之前呼叫了set
方法就不會呼叫initialValue
。一般來說該方法只會被呼叫一次,但如果使用remove
清空隨後又呼叫get
方法時,又會再次呼叫initialValue
。
如果我們想設一個初值,一般就是用匿名內部類的方式重寫該方法實現自定義初值,比如:
ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
} };
3.3.2 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();
}
get
方法很重要,他被用來獲取當前thread私有的threadlocal
的變數副本。 如果變數沒有當前執行緒的值,則首先將其初始化為呼叫initialValue
方法的返回值。
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;
}
這個是剛剛get
方法裡使用的,可以看到他會先呼叫initialValue
方法去拿到初始值,然後獲取當前執行緒的ThreadLocalMap
。如果map不存在就以當前thread
和value
建立ThreadLocalMap;如果已經存在就以當前ThreadLocal
例項為key,value為值放入該ThreadLocalMap。
3.3.3 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);
}
跟前面提到的setInitialValue
方法差不多,只不過這裡指定了value。
3.3.4 remove
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
獲取當前執行緒的ThreadLocalMap,然後從其中移除當前ThreadLocal例項為key的Entry
。
3.4 內部類
3.4.1 SuppliedThreadLocal
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
return supplier.get();
}
}
這個類的主要作用是配合withInitial
設定初始值,示例如下:
private static ThreadLocal<Integer> ti1 = ThreadLocal.withInitial(new Supplier<Integer>() {
@Override
public Integer get()
{
return 33;
}
});
private static ThreadLocal<Integer> ti2 = ThreadLocal.withInitial(() -> 44);
3.4.2 ThreadLocalMap
ThreadLocalMap是一個自定義的hasmap
,他只適合維護threadlocal values
,沒有向外暴露任何方法。
為了應對長期和高負荷的使用,所以採用了WeakReference
來修飾該map的key。也就是說當這些key無其他強引用時,GC會將他們回收。注意,
0x05 坑
5.1 記憶體洩露
因為每個執行緒都有一個map,指向使用的ThreadLocal
物件,而且這是一個強引用。也就是說,當一個ThreadLocal強引用持續存在時,使用了該ThreadLocal的執行緒的ThreadLocalMap裡的Entry之key雖是弱引用指向該ThreadLocal物件,但是因為還有強引用存在的關係就一直不會被回收,該entry也會隨著執行緒持續存在而存在,造成記憶體洩露。所以我們應該在每個執行緒使用完ThreadLocal物件後呼叫remove
方法,手動移除該entry。
5.2 執行緒池
Thread物件就那麼幾個,都是複用的。也就是說,他們的ThreadLocalMap是不會變的,就會導致其他Runnable的ThreadLocal值交叉混用,出現問題。
0x06 例子
6.1 DateFormat
SimpleDateFormat並不是執行緒安全的,所以在阿里的Java開發規範裡推薦了用ThreadLocal保證執行緒安全的做法:
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
} };
String startTimeStr = "2018-01-11 02:17:02.806";
try {
Date startTime = df.get().parse(startTimeStr);
System.out.println("startTime=" + startTime);
String dfStr = df.get().format(new Date());
System.out.println("dfStr=" + dfStr);
}catch (ParseException e) {
e.printStackTrace();
}
0xFF 參考文件
JavaDoc-java.lang.ThreadLocal