ThreadLocal類的簡單介紹
在併發編程中我們經常有共享資源的需求,而通過用鎖的形式來保證資源的安全在多個方法共同讀寫同一資源時很難得到保證。再者,若全局共享一份資源,根據訪問者的不同來處理不同的邏輯也將變的很困難。這時我們的ThreadLocal類就可以大顯身手了,它可以爲使用相同變量的每個線程都創建不同的副本。說起來總是很抽象,我們先來看一段實例代碼:
public class ThreadLocalTest {
private static ThreadLocal<String> myData = new ThreadLocal<String>();
public static void main(String[] args) {
myData.set("主線程存儲內容");
new Thread(new Runnable() {
public void run() {
myData.set("子線程1存儲內容");
System.out.println(myData.get());
}
},"thread_1").start();
new Thread(new Runnable() {
public void run() {
myData.set("子線程2存儲內容");
System.out.println(myData.get());
}
},"thread_2").start();
new Thread(new Runnable() {
public void run() {
System.out.println(myData.get());
}
},"thread_3").start();
System.out.println(myData.get());
}
}
輸出:
子線程1存儲內容
主線程存儲內容
null
子線程2存儲內容
代碼很好理解,但是有沒有感覺一點困惑?對,在不同的線程訪問同一對象會返回不同的結果,並且如果在當前線程沒有爲這個對象來設置值的話,那麼將默認返回null。顯然ThreadLocal類爲每一個線程單獨儲存了一份數據。那麼其內部又是如何實現的?請看源碼分析:ThreadLocal源代碼分析
爲了更好的理解代碼的意圖也避免看一些細枝末節的源代碼看的心煩,我們不去深究代碼的一些細節,並且從我們第一次ThreadLocal的地方來入手。由上面的例子可知,我們第一次調用的ThreadLocal的方法是set,我們先來看一下它的代碼:
public void set(T value) {
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values == null) {
values = initializeValues(currentThread);
}
values.put(this, value);
}
一步一步的來分析這段代碼:
首先,獲得了當前調用此方法的線程對象,然後調用了values方法,並將其傳入,來看一下values方法的代碼:
Values values(Thread current) {
return current.localValues;
}
返回了線程類的localValues對象,我們接着打開線程類來找找這個對象:
ThreadLocal.Values localValues;
線程類中只聲明瞭這個對象的引用,並且在之後的任何地方都沒有用到它,那我們得到的這個values對象也就是null了。繼續看,很順利的進入到了if代碼塊中,我們來看一下initializeValues方法:
Values initializeValues(Thread current) {
return current.localValues = new Values();
}
爲當前線程中的localValues引用new了對象。而且這個對象的類型是Values竟是ThreadLocal中的類:
/**
* Per-thread map of ThreadLocal instances to values.
*/
static class Values {
}
感覺非常的奇怪,爲什麼繞了一個這麼大的圈子然後最後new出了一個靜態的內部類?仔細想一下便不難理解,因爲ThreadLocal要爲每個線程單獨的保存數據,那麼這個保存數據的對象就不能作爲它的對象存在,不然也就沒啥意義了,那麼最好的方法也就是作爲線程類的對象存在。現在values也有了,然後調用了put方法,我們看一下源碼:
void put(ThreadLocal<?> key, Object value) {
cleanUp();
// Keep track of first tombstone. That's where we want to go back
// and add an entry if necessary.
int firstTombstone = -1;
for (int index = key.hash & mask;; index = next(index)) {
Object k = table[index];
if (k == key.reference) {
// Replace existing entry.
table[index + 1] = value;
return;
}
if (k == null) {
if (firstTombstone == -1) {
// Fill in null slot.
table[index] = key.reference;
table[index + 1] = value;
size++;
return;
}
// Go back and replace first tombstone.
table[firstTombstone] = key.reference;
table[firstTombstone + 1] = value;
tombstones--;
size++;
return;
}
// Remember first tombstone.
if (firstTombstone == -1 && k == TOMBSTONE) {
firstTombstone = index;
}
}
}
我們挑重點的看一下,首先通過key和mask找到了一個位置(index),而後從table數組中找到這個位置中存放的數據。下面在if語句中判斷這個數據是不是等於key.reference,那麼這個key是什麼,reference有事什麼?可以看到,key是set方法傳入的參數,而調用set方法是傳入的參數是this,也就是當前對象本身。而reference參數可以看一下定義的源碼:
private final Reference<ThreadLocal<T>> reference
= new WeakReference<ThreadLocal<T>>(this);
很顯然我們之前沒有設置過,現在的k並沒有等於當前的reference,繼續向下來看,當k爲空時,給k設置了值,並且將我們想要放到ThreadLocal中的對象放到了table數組中index+1的位置。set方法現在應該是很清晰了,我們來看一下get:
public T get() {
// Optimized for the fast path.
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values != null) {
Object[] table = values.table;
int index = hash & values.mask;
if (this.reference == table[index]) {
return (T) table[index + 1];
}
} else {
values = initializeValues(currentThread);
}
return (T) values.getAfterMiss(this);
}
有了剛纔的分析,我們不難看懂上面的代碼,它將我們剛纔放進去的值取出來了。但是如果values爲空的時候返回的是什麼?我們來看一下getAfterMiss方法:
Object getAfterMiss(ThreadLocal<?> key) {
...
if (table[index] == null) {
Object value = key.initialValue();
// If the table is still the same and the slot is still empty...
if (this.table == table && table[index] == null) {
table[index] = key.reference;
table[index + 1] = value;
size++;
cleanUp();
return value;
}
// The table changed during initialValue().
put(key, value);
return value;
}
...
}
方法比較長,我截取出了其中我們想要的一段,如果有興趣讀者可以自己去細緻分析。在代碼段裏顯示返回了key.initialValue()方法的返回值,我們來看一下:
protected T initialValue() {
return null;
}
這個返回了個null,不知道大家還記不記得最開始貼出的示例代碼的運行結果,在沒有set值時,get方法返回的是null,這也印證了我們的結論。