什麼是ThreadLocal?
關于ThreadLocal的知識網上有很多,但參差不齊很片面,看了很多部落格後發現有一篇寫的很全面客觀,貼出來大家可以自行觀看:http://www.iteye.com/topic/103804
下面講一下我自己的了解:線程本地存儲區(Thread Local Storage,簡稱為TLS),每個線程都有自己的私有的本地存儲區域,不同線程之間彼此不能通路對方的TLS區域。ThreadLocal不是用來解決共享對象的多線程通路問題的,一般情況下,通過ThreadLocal.set() 到線程中的對象是該線程自己使用的對象,其他線程是不需要通路的,也通路不到的。各個線程中通路的是不同的對象。
ThreadLocal的結構
首先我們來看一下ThreadLocal這個類的結構:
可以看到除了ThreadLocal的一些方法和變量之外,還有兩個靜态内部類:ThreadLocalMap和SuppliedThreadLocal,其中ThreadLocal是實作的關鍵,我們來看一下他的結構:
ThreadLocalMap裡還有一個内部類Entry,其實每個線程存的值都在這裡實作:
ThreadLocal執行個體
如果隻是單純的講這個類難免枯燥,我們結合一個執行個體來講;比如我們要在一個類中放一個String類型的字元串,讓每個線程都能設定和獲得不同的字元串,我們可以這樣寫:
public class Test {
public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
static class Thread1 extends Thread{
@Override
public void run() {
//在新線程中設定值
threadLocal.set("a");
System.out.println("線程:"+Thread.currentThread()+"值:"+threadLocal.get());
}
}
public static void main(String[] args) {
//在主線程中設定值
threadLocal.set("main");
Thread1 thread1 = new Thread1();
thread1.start();
System.out.println("線程:"+Thread.currentThread()+"值:"+threadLocal.get());
}
}
首先在Test類中建立一個ThreadLocal對象,指定泛型類型為String,然後在Test類中建立一個靜态内部類Thread1,在他的run方法中給ThreadLocal設定值,然後取出值。在mian方法中首先給ThreadLocal設定一個值,然後開啟Thread1線程,運作結果如下:
線程:Thread[main,,main]值:main
線程:Thread[Thread-,,main]值:a
可以看到主線程中設定的值和主線程中列印的值是相對應的,而新線程中設定的值和新線程中列印的值是相對應的,這就是ThreadLocal。
執行個體分析
下面我們對這個執行個體進行分析,看ThreadLocal是如何在不同的線程中取出不同的值的:
在我們new出一個Threadlocal執行個體時,ThreadLocal做了那些操作呢,我們看一下他的構造函數:
/**
* Creates a thread local variable.
*/
public ThreadLocal() {
}
我們可以看到他的構造函數是空的,也就是說當我們new一個執行個體時隻是初始化了成員變量,我們看一下他的靜态代碼和成員變量:
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 = ;
/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
其中有隻有一個threadLocalHashCode是在建立對象時初始化的,是以ThreadLocal執行個體的變量隻有這個threadLocalHashCode,而且是final的,用來區分不同的ThreadLocal執行個體,至于為什麼要這樣一個變量後面會詳細說。
接下來通過threadLocal.set(“main”)來設定值,這個方法很關鍵:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
首先擷取到目前線程的執行個體,然後在通過 getMap(t)方法擷取到與線程綁定的ThreadLocalMap執行個體,如果擷取到的執行個體不為空,則通過ThreadLocalMap的set方法設定值,否則建立一個ThreadLocalMap執行個體。
我雖然隻用幾句話總結了這個方法,但其實裡面的具體實作還是挺複雜的,我們來看一下 getMap(t)和createMap(t, value)這兩個方法:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
getMap是傳回一個ThreadLocalMap執行個體,而createMap是建立一個ThreadLocalMap執行個體,注意看建立的執行個體是放在那裡的,t.threadLocals也就是Thread裡的變量,我們看一下Thread類裡的這個屬性:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
這下就清楚了,我們儲存值的時候,首先會通過Thread.currentThread()擷取目前線程的執行個體,然後通過這個執行個體的getMap(t)方法擷取線程裡存放的ThreadLocalMap對象,如果目前線程裡的ThreadLocalMap對象為空則建立一個執行個體放到該線程中,這樣每個線程都擁有一個不同的ThreadLocalMap執行個體,而這個執行個體就是不同線程儲存不同值的關鍵。搞清了這一點就基本了解ThreadLocal的原理了。
接着我們在看一下ThreadLocalMap的構造函數:
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - );
table[i] = new Entry(firstKey, firstValue);
size = ;
setThreshold(INITIAL_CAPACITY);
}
首先初始化一個Entry數組,關于Entry其實他就相當于一個鍵值對,ThreadLocal是他的鍵(key),我們存放的值是他的值(values),原來我們的值最終是存到了這個Entry中了。然後通過ThreadLocal的threadLocalHashCode 屬性計算出一個值,還記得這個變量嗎,其實他的作用就是映射一個ThreadLocal在Entry數組中的位置,由于每個Threadlocal的threadLocalHashCode值是不一樣的,這樣當我們取值的時候,通過這個threadLocalHashCode 就可以找到我們存儲的Entry對象的位置,這裡也許有人會好奇了,為什麼不直接通過key也就是ThreadLocal在數組中查詢呢,hashMap不就是這樣做的嗎,其實hashMap底層也是使用的hash表來查詢的,這樣做的好處是查詢快。我們通過threadLocalHashCode計算出了目前ThreadLocal的Entry對象在Entry數組中的位置,接着我們就把Entry對象初始化然後放到數組的對應位置。
為什麼要用一個Entry數組呢?直接将值存到一個Entry對象中,然後ThreadLocalMap持有這個執行個體不就行了嗎?剛開始我也在這裡糾結了好久,但是仔細一想,如果我們線程要存兩個值,但是一個線程隻有一個ThreadLocalMap執行個體,這顯然就不行了,是以要用Entry數組,将不同的值存到數組中,例:
public class Test {
public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
static class Thread1 extends Thread{
@Override
public void run() {
//在新線程中設定值
threadLocal.set("a");
threadLocal2.set();
System.out.println("線程:"+Thread.currentThread()+"值:"+threadLocal.get());
System.out.println("線程:"+Thread.currentThread()+"值:"+threadLocal2.get());
}
}
public static void main(String[] args) {
//在主線程中設定值
threadLocal.set("main");
threadLocal2.set();
Thread1 thread1 = new Thread1();
thread1.start();
System.out.println("線程:"+Thread.currentThread()+"值:"+threadLocal.get());
System.out.println("線程:"+Thread.currentThread()+"值:"+threadLocal2.get());
}
}
運作結果:
線程:Thread[main,,main]值:main
線程:Thread[Thread-,,main]值:a
線程:Thread[main,,main]值:
線程:Thread[Thread-,,main]值:
現在還差一個map.set(this, value)方法:
private void set(ThreadLocal key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
這個方法其實就是省去了初始化Entry數組的過程,直接通過threadLocalHashCode計算出了目前ThreadLocal的Entry對象在Entry數組中的位置。
這裡我們先總結幾個知識點:
- ThreadLocal的構造函數是的,在初始化的時候隻初始化一個threadLocalHashCode變量,這是唯一辨別目前ThreadLocal對象的
- ThreadLocalMap的執行個體是存放在Thread中的,ThreadLocalMap的構造函數接收兩個參數:ThreadLocal和values
- 我們存的值最終是以鍵值對的形式存在Entry對象中的,ThreadLocal是鍵,values是值
- ThreadLocal進行set操作時會先擷取目前線程執行個體中的ThreadLocalMap對象,然後将值設定到ThreadLocalMap中存儲的Entry數組的Entry對象中。
- Entry對象存到Entry數組中的位置是由threadLocalHashCode計算得到的,這樣做是為了節省查找時間,不懂的百度一下hash表
- Entry數組是為了實作一個線程存儲多個值
下面我們看一下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)
return (T)e.value;
}
return setInitialValue();
}
get方法也是要先擷取目前線程的執行個體,然後在通過 getMap(t)方法擷取到與線程綁定的ThreadLocalMap執行個體,如果擷取到的執行個體不為空則通過這個ThreadLocalMap的getEntry()方法擷取Entry執行個體,然後直接就擷取了Entry執行個體中的值就可以了,如果ThreadLocalMap為空就初始化一個null值傳回。
下面我們來詳細分析一下get方法, getMap(t)不用講了,和之前set裡的一樣,我們來看一下 map.getEntry(this)方法:
private Entry getEntry(ThreadLocal key) {
int i = key.threadLocalHashCode & (table.length - );
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
這裡就用到threadLocalHashCode 來擷取Entry對象在Entry數組中的位置,隻是簡單的數學計算就可以完成,可以說根本不用時間,但是如果一個個取出Entry對象再一個個比較他們的key,這浪費的時間可想而知,取出Entry對象後判斷是否為空,并且判斷一下取出的key是否是和目前ThreadLocal一樣 ,判斷完成後傳回Entry對象就ok了,至于如果Entry為空或者key值不一樣的處理大家自己去看getEntryAfterMiss(key, i, e)的源碼,我就不講了,這種情況一般不會出現。
我們再看一下setInitialValue()方法:
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方法給ThreadLocal設定值,這時是沒有值能夠取出來的,是以就調用一下該方法初始一個值,這個方法的第一行就是調用一個initialValue()方法,這個方法很重要,我們看一下他的實作:
protected T initialValue() {
return null;
}
傳回空,沒錯因為我們沒有值隻能傳回空,注意這個方法是可以重寫的,我們可以自定義他的預設值:
public static ThreadLocal<String> threadLocal = new ThreadLocal<String>(){
@Override
protected String initialValue() {
return "defaultValues";
}
};
public static void main(String[] args) {
//在主線程中設定值
// threadLocal.set("main");
threadLocal2.set();
Thread1 thread1 = new Thread1();
thread1.start();
System.out.println("線程:"+Thread.currentThread()+"值:"+threadLocal.get());
System.out.println("線程:"+Thread.currentThread()+"值:"+threadLocal2.get());
}
線程:Thread[main,,main]值:defaultValues
線程:Thread[main,,main]值:
線程:Thread[Thread-,,main]值:a
線程:Thread[Thread-,,main]值:
在調用了initialValue()方法之後再次對ThreadLocalMap是否為空做一次判斷,然後掉用 map.set(this, value)或者createMap(t, value)方法将我們初始化的值設定到Entry中。
這裡再總結幾個知識點:
- ThreadLocal進行get操作時也會先擷取目前線程執行個體中的ThreadLocalMap對象,然後通過該對象擷取Entry,進而擷取到存儲的值。
- ThreadLocal是有預設的值的,可以自定義也可以為空,預設的值也會存入Entry對象中。
到這裡ThreadLocal的原理已經講完了,可能有人會說這個東西根本沒用到過,我們學他幹嘛,我想說我們看源碼更多的不是為了去使用它,如果隻是ThreadLocal的使用我想幾百字就解決了,我們是為了了解他的思想和解決問題的思路,這才是能提升我們的東西。