Java ThreadLocal 使用詳解

併發程式設計網發表於2015-09-09

引言

ThreadLocal的官方API解釋為:

“該類提供了執行緒區域性 (thread-local) 變數。這些變數不同於它們的普通對應物,因為訪問某個變數(通過其 get 或 set 方法)的每個執行緒都有自己的區域性變數,它獨立於變數的初始化副本。ThreadLocal 例項通常是類中的 private static 欄位,它們希望將狀態與某一個執行緒(例如,使用者 ID 或事務 ID)相關聯。”

大概的意思有兩點:

  1. ThreadLocal提供了一種訪問某個變數的特殊方式:訪問到的變數屬於當前執行緒,即保證每個執行緒的變數不一樣,而同一個執行緒在任何地方拿到的變數都是一致的,這就是所謂的執行緒隔離。
  2. 如果要使用ThreadLocal,通常定義為private static型別,在我看來最好是定義為private static final型別。

應用場景

ThreadLocal通常用來共享資料,當你想在多個方法中使用某個變數,這個變數是當前執行緒的狀態,其它執行緒不依賴這個變數,你第一時間想到的就是把變數定義在方法內部,然後再方法之間傳遞引數來使用,這個方法能解決問題,但是有個煩人的地方就是,每個方法都需要宣告形參,多處宣告,多處呼叫。影響程式碼的美觀和維護。有沒有一種方法能將變數像private static形式來訪問呢?這樣在類的任何一處地方就都能使用。這個時候ThreadLocal大顯身手了。

實踐

我們首先來看一段程式碼:

import java.util.HashMap;

import java.util.Map;

 

public class TreadLocalTest {

// static ThreadLocal<HashMap> threadLocal = new ThreadLocal<HashMap>(){

// @Override

// protected HashMap initialValue() {

// System.out.println(Thread.currentThread().getName()+”initialValue”);

// return new HashMap();

// }

// };

 

public static class T1 implements Runnable {

private final static Map map = new HashMap();

int id;

 

public T1(int id) {

this.id = id;

}

public void run() {

// Map map = threadLocal.get();

for (int i = 0; i < 20; i++) {

map.put(i, i + id * 100);

try {

Thread.sleep(100);

} catch (Exception ex) {

}

}

System.out.println(Thread.currentThread().getName()

+ “# map.size()=” + map.size() + ” # ” + map);

}

}

public static void main(String[] args) {

Thread[] runs = new Thread[15];

T1 t = new T1(1);

for (int i = 0; i < runs.length; i++) {

runs[i] = new Thread(t);

}

for (int i = 0; i < runs.length; i++) {

runs[i].start();

}

}

}

這段程式的本意是,啟動15個執行緒,執行緒向map中寫入20個整型值,然後輸出map。執行該程式,觀察結果,我們會發現,map中壓根就不止20個元素,這說明程式產生了執行緒安全問題。

我們都知道HashMap是非執行緒安全的,程式啟動了15個執行緒,他們共享了同一個map,15個執行緒都往map寫物件,這勢必引起執行緒安全問題。

我們有兩種方法解決這個問題:

  1. 將map的宣告放到run方法中,這樣map就成了方法內部變數,每個執行緒都有一份new HashMap(),無論多少個執行緒執行run方法,都不會有執行緒安全問題。這個方法也正如應用場景中提到的,如果有多處地方使用到map,傳值是個煩人的地方。
  2. 將HashMap換成Hashtable。用執行緒同步來解決問題,然而我們的程式只是想向一個map中寫入20個整型的KEY-VALUE而已,並不需要執行緒同步,同步勢必影響效能,得不償失。
  3. ThreadLocal提供另外一種解決方案,即在解決方案a上邊,將new HashMap()得到的例項變數,繫結到當前執行緒中。之後從任何地方,都可以通過ThreadLocal獲取到該變數。將程式中的註釋程式碼恢復,再將 private final static Map map = new HashMap();註釋掉,執行程式,結果就是我們想要的。

實現原理

程式呼叫了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();

}

getMap方法的原始碼:

ThreadLocalMap getMap(Thread t) {

return t.threadLocals;

}

該方法返回的是當前執行緒中的ThreadLocalMap例項。閱讀Thread的原始碼我們發現Thread中有如下變數宣告:

/* ThreadLocal values pertaining to this thread. This map is maintained

* by the ThreadLocal class. */

ThreadLocal.ThreadLocalMap threadLocals = null;

我們暫時可以將ThreadLocalMap理解為一個類似Map的這麼個類,之後再講解它。

get()方法的大致意思就是從當前執行緒中拿到ThreadLocalMap的例項threadLocals,如果threadLocals不為空,那麼就以當前ThreadLocal例項為KEY從threadLocals中拿到對應的VALUE。如果不為空,那麼就呼叫 setInitialValue()方法初始化threadLocals,最終返回的是initialValue()方法的返回值。下面是 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;

}

我們看到map.set(this, value);這句程式碼將ThreadLocalMap的例項作為KEY,將initialValue()的返回值作為VALUE,set到了threadLocals中。

程式在宣告ThreadLocal例項的時候覆寫了initialValue(),返回了VALUE,當然我們可以直接呼叫set(T t)方法來設定VALUE。下面是set(T t)方法的原始碼:

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()方法就少了個return語句。這兩種方式都能達到初始化ThreadLocalMap例項的效果。

我們再來看一下ThreadLocal類的結構。

ThreadLocal類只有三個屬性,如下:

/*ThreadLocal的hash值,map用它來儲存值*/

private final int threadLocalHashCode = nextHashCode();

/*改類能以原子的方式更新int值,這裡主要是在產生新的ThreadLocal例項時用來產生一個新的hash值,map用該值來儲存物件*/

private static AtomicInteger nextHashCode =

new AtomicInteger();

/*該變數標識每次產生新的ThreadLocal例項時,hash值的增量*/

private static final int HASH_INCREMENT = 0x61c88647;

剩下的就是一些方法。最關鍵的地方就是ThreadLocal定義了一個靜態內部類ThreadLocalMap。我們在下一章節再來分析這個類。從ThreadLocal的類結構,我們可以看到,實際上問題的關鍵先生是ThreadLocalMap,ThreadLocal只是提供了管理的功能,我們也可以說ThreadLocal只是代理了ThreadLocalMap而已。

ThreadLocalMap原始碼分析

既然ThreadLocalMap實現了類似map的功能,那我們首先來看看它的set方法原始碼:

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-1);

 

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();

}

這個方法的主要功能就是講KEY-VALUE儲存到ThreadLocalMap中,這裡至少我們看到KEY實際上是 key.threadLocalHashCode,ThreadLocalMap同樣維護著Entry陣列,這個Entry我們在下一節會講解。這裡涉及到了Hash衝突的處理,這裡並不會向HashMap一樣衝突了以連結串列的形式往後新增。如果對這個Hash衝突解決方案有興趣,可以再進一步研究原始碼。

既然ThreadLocalMap也是用Entry來儲存物件,那我們來看看Entry類的宣告,Entry被定義在ThreadLocalMap的內部:

static class Entry extends WeakReference<ThreadLocal> {

/** The value associated with this ThreadLocal. */

Object value;

 

Entry(ThreadLocal k, Object v) {

super(k);

value = v;

}

}

這裡我們看到Entry整合了WeakReference類,泛型宣告瞭ThreadLocal,即每一個Entry物件都保留了對 ThreadLocal例項的弱引用,之所以這麼幹的原因是,執行緒在結束之後需要將ThreadLocal例項從map中remove調,以便回收記憶體空間。

總結

首先,ThreadLocalMap並不是為了解決執行緒安全問題,而是提供了一種將例項繫結到當前執行緒的機制,類似於隔離的效果,實際上自己在方法中new出來變數也能達到類似的效果。ThreadLocalMap跟執行緒安全基本不搭邊,繫結上去的例項也不是多執行緒公用的,而是每個執行緒new一份,這個例項肯定不是共用的,如果共用了,那就會引發執行緒安全問題。ThreadLocalMap最大的用處就是用來把例項變數共享成全域性變數,在程式的任何方法中都可以訪問到該例項變數而已。網上很多人說ThreadLocalMap是解決了執行緒安全問題,其實是望文生義,兩者不是同類問題。

相關文章