ThreadLocal是什麼
以前面試的時候問到ThreadLocal總是一臉懵逼,只知道有這個哥們,不瞭解他是用來做什麼的,更不清楚他的原理了。表面上看他是和多線程,線程同步有關的一個工具類,但其實他與線程同步機制無關。線程同步機制是多個線程共享同一個變量,而ThreadLocal是爲每個線程創建一個單獨的變量副本,每個線程都可以改變自己的變量副本而不影響其它線程所對應的副本。
官方API上是這樣介紹的:該類提供了線程局部(thread-local)變量。這些變量不同於它們的普通對應物,因爲訪問某個變量(通過其 get 或 set 方法)的每個線程都有自己的局部變量,它獨立於變量的初始化副本。ThreadLocal實例通常是類中的 private static 字段,它們希望將狀態與某一個線程(例如,用戶 ID 或事務 ID)相關聯。
ThreadLocal的API
ThreadLocal定義了四個方法:
-
get():返回此線程局部變量當前副本中的值
-
set(T value):將線程局部變量當前副本中的值設置爲指定值
-
initialValue():返回此線程局部變量當前副本中的初始值
-
remove():移除此線程局部變量當前副本中的值
ThreadLocal還有一個特別重要的靜態內部類ThreadLocalMap,該類纔是實現線程隔離機制的關鍵。get()、set()、remove()都是基於該內部類進行操作,ThreadLocalMap用鍵值對方式存儲每個線程變量的副本,key爲當前的ThreadLocal對象,value爲對應線程的變量副本。
試想,每個線程都有自己的ThreadLocal對象,也就是都有自己的ThreadLocalMap,對自己的ThreadLocalMap操作,當然是互不影響的了,這就不存在線程安全問題了,所以ThreadLocal是以空間來交換安全性的解決思路。
使用實例
假設每個線程都需要一個計數值記錄自己做某件事做了多少次,各線程運行時都需要改變自己的計數值而且相互不影響,那麼ThreadLocal就是很好的選擇,這裏ThreadLocal裏保存的當前線程的局部變量的副本就是這個計數值。
public class SeqCount {
private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>() {
@Override
protected 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 seqThread1 = new SeqThread(seqCount);
SeqThread seqThread2 = new SeqThread(seqCount);
SeqThread seqThread3 = new SeqThread(seqCount);
SeqThread seqThread4 = new SeqThread(seqCount);
seqThread1.start();
seqThread2.start();
seqThread3.start();
seqThread4.start();
}
public static class SeqThread extends Thread {
private SeqCount seqCount;
public SeqThread(SeqCount seqCount) {
this.seqCount = seqCount;
}
@Override
public void run() {
for (int i=0; i<3; i++) {
System.out.println(Thread.currentThread().getName()+" seqCount:"+seqCount.nextSeq());
}
}
}
}
運行結果:
解決SimpleDateFormat的線程安全
我們知道SimpleDateFormat在多線程下是存在線程安全問題的,那麼將SimpleDateFormat作爲每個線程的局部變量的副本就是每個線程都擁有自己的SimpleDateFormat,就不存在線程安全問題了。
public class SimpleDateFormatDemo {
private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<>();
/**
* 獲取線程的變量副本,如果不覆蓋initialValue方法,第一次get將返回null,故需要創建一個DateFormat,放入threadLocal中
* @return
*/
public DateFormat getDateFormat() {
DateFormat df = threadLocal.get();
if (df == null) {
df = new SimpleDateFormat(DATE_FORMAT);
threadLocal.set(df);
}
return df;
}
public static void main(String [] args) {
SimpleDateFormatDemo formatDemo = new SimpleDateFormatDemo();
MyRunnable myRunnable1 = new MyRunnable(formatDemo);
MyRunnable myRunnable2 = new MyRunnable(formatDemo);
MyRunnable myRunnable3 = new MyRunnable(formatDemo);
Thread thread1= new Thread(myRunnable1);
Thread thread2= new Thread(myRunnable2);
Thread thread3= new Thread(myRunnable3);
thread1.start();
thread2.start();
thread3.start();
}
public static class MyRunnable implements Runnable {
private SimpleDateFormatDemo dateFormatDemo;
public MyRunnable(SimpleDateFormatDemo dateFormatDemo) {
this.dateFormatDemo = dateFormatDemo;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 當前時間:"+dateFormatDemo.getDateFormat().format(new Date()));
}
}
}
運行結果:
源碼分析
ThreadLocalMap
ThreadLocalMap內部是利用Entry來進行key-value的存儲的。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
上面源碼中key就是ThreadLocal,value就是值,Entry繼承WeakReference,所以Entry對應key的引用(ThreadLocal實例)是一個弱引用。
set(ThreadLocal key, Object value)
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//根據ThreadLocal的散列值,查找對應元素在數組中的位置
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),說明之前的ThreadLocal對象已經被回收了
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//ThreadLocal對應的key實例不存在,new一個
tab[i] = new Entry(key, value);
int sz = ++size;
//清楚陳舊的Entry(key == null的)
// 如果沒有清理陳舊的 Entry 並且數組中的元素大於了閾值,則進行 rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
這個set操作和集合Map解決散列衝突的方法不同,集合Map採用的是鏈地址法,這裏採用的是開放定址法(線性探測)。set()方法中的replaceStaleEntry()和cleanSomeSlots(),這兩個方法可以清除掉key ==null的實例,防止內存泄漏。
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);
}
由於採用了開放定址法,當前keu的散列值和元素在數組中的索引並不是一一對應的,首先取一個猜測數(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是我們要找的。這裏當key==null時,調用expungeStaleEntry有利於GC的回收,用於防止內存泄漏。
ThreadLocal爲什麼會內存泄漏
ThreadLocalMap的key爲ThreadLocal實例,他是一個弱引用,我們知道弱引用有利於GC的回收,當key == null時,GC就會回收這部分空間,但value不一定能被回收,因爲他和Current Thread之間還存在一個強引用的關係。由於這個強引用的關係,會導致value無法回收,如果線程對象不消除這個強引用的關係,就可能會出現OOM。有些時候,我們調用ThreadLocalMap的remove()方法進行顯式處理。
總結
ThreadLocal不是用來解決共享變量的問題,也不是協調線程同步,他是爲了方便各線程管理自己的狀態而引用的一個機制。
每個ThreadLocal內部都有一個ThreadLocalMap,他保存的key是ThreadLocal的實例,他的值是當前線程的局部變量的副本的值。