Java ThreadLocal 使用詳解
引言
ThreadLocal的官方API解釋為:
“該類提供了執行緒區域性 (thread-local) 變數。這些變數不同於它們的普通對應物,因為訪問某個變數(通過其 get 或 set 方法)的每個執行緒都有自己的區域性變數,它獨立於變數的初始化副本。ThreadLocal 例項通常是類中的 private static 欄位,它們希望將狀態與某一個執行緒(例如,使用者 ID 或事務 ID)相關聯。”
大概的意思有兩點:
- ThreadLocal提供了一種訪問某個變數的特殊方式:訪問到的變數屬於當前執行緒,即保證每個執行緒的變數不一樣,而同一個執行緒在任何地方拿到的變數都是一致的,這就是所謂的執行緒隔離。
- 如果要使用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寫物件,這勢必引起執行緒安全問題。
我們有兩種方法解決這個問題:
- 將map的宣告放到run方法中,這樣map就成了方法內部變數,每個執行緒都有一份new HashMap(),無論多少個執行緒執行run方法,都不會有執行緒安全問題。這個方法也正如應用場景中提到的,如果有多處地方使用到map,傳值是個煩人的地方。
- 將HashMap換成Hashtable。用執行緒同步來解決問題,然而我們的程式只是想向一個map中寫入20個整型的KEY-VALUE而已,並不需要執行緒同步,同步勢必影響效能,得不償失。
- 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是解決了執行緒安全問題,其實是望文生義,兩者不是同類問題。
相關文章
- Java 之 ThreadLocal 詳解Javathread
- Java中的ThreadLocal詳解Javathread
- Threadlocal詳解(ThreadLocal,InheritTableThreadLocal,TransmittableThreadLocal)threadMIT
- JUC---ThreadLocal原理詳解thread
- ThreadLocal原理用法詳解ThreadLocal記憶體洩漏thread記憶體
- Java Stream 使用詳解Java
- Java併發程式設計:執行緒封閉和ThreadLocal詳解Java程式設計執行緒thread
- JAVA列舉使用詳解Java
- ThreadLocal在java web工程中的使用。threadJavaWeb
- Java 內部類使用詳解Java
- Java的jinfo命令使用詳解Java
- Java的jmap命令使用詳解Java
- java_clone方法使用詳解Java
- Java 本地介面 JNI 使用詳解Java
- Java註解處理器使用詳解Java
- Java ThreadLocal解析Javathread
- Java - ThreadLocal類Javathread
- Java的jstat命令使用詳解JavaJS
- Java的jstack命令使用詳解JavaJS
- Java同步塊(synchronized block)使用詳解JavasynchronizedBloC
- 執行緒封閉之ThreadLocal原始碼詳解執行緒thread原始碼
- Java ThreadLocal深度解析Javathread
- ThreadLocal的使用thread
- java enum(列舉)使用詳解 + 總結Java
- Java 8中的default方法使用詳解Java
- Java File 類的使用方法詳解Java
- 使用Java填充Word模板的方法詳解Java
- Java多執行緒10:ThreadLocal的作用及使用Java執行緒thread
- Java併發——ThreadLocal分析Javathread
- Java基礎(1)——ThreadLocalJavathread
- java之ThreadLocal筆記Javathread筆記
- Java註解詳解Java
- Java 註解詳解Java
- Java直接(堆外)記憶體使用詳解Java記憶體
- Java 執行緒 Executor 框架詳解與使用Java執行緒框架
- Java記憶體分析利器MAT使用詳解Java記憶體
- java反射詳解Java反射
- Java Stream 詳解Java