之前面試的時候問到ThreadLocal老是一臉懵逼,只知道有這個哥們,不瞭解他是用來作什麼的,更不清楚他的原理了。表面上看他是和多線程,線程同步有關的一個工具類,但其實他與線程同步機制無關。線程同步機制是多個線程共享同一個變量,而ThreadLocal是爲每一個線程建立一個單獨的變量副本,每一個線程均可以改變本身的變量副本而不影響其它線程所對應的副本。
官方API上是這樣介紹的:該類提供了線程局部(thread-local)變量。這些變量不一樣於它們的普通對應物,由於訪問某個變量(經過其 get 或 set 方法)的每一個線程都有本身的局部變量,它獨立於變量的初始化副本。ThreadLocal實例一般是類中的 private static 字段,它們但願將狀態與某一個線程(例如,用戶 ID 或事務 ID)相關聯。面試
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());
}
}
}
}
複製代碼
運行結果:bash
咱們知道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()));
}
}
}
複製代碼
運行結果:ide
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 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的實例,防止內存泄漏。ui
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是咱們要找的元素,那麼直接返回,不然調用getEntryAfterMissthis
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的回收,用於防止內存泄漏。
ThreadLocalMap的key爲ThreadLocal實例,他是一個弱引用,咱們知道弱引用有利於GC的回收,當key == null時,GC就會回收這部分空間,但value不必定能被回收,由於他和Current Thread之間還存在一個強引用的關係。因爲這個強引用的關係,會致使value沒法回收,若是線程對象不消除這個強引用的關係,就可能會出現OOM。有些時候,咱們調用ThreadLocalMap的remove()方法進行顯式處理。