下面我們就來看看道哥都用的ThreadLocal。
1 ThreadLocal你來自哪里
Since: 1.2
Author: Josh Bloch and Doug Lea
又是并發大佬們的杰作,膜拜一下。怪不得道哥也愛用,自己設計的類總得用用。下面來看看基本內容與用法吧。
2 ThreadLocal原理
“This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).”
“此類提供了thread-local變量。這些變量不同于普通的類似變量,因為訪問某個變量(通過其 get 或 set 方法)的每個線程都有自有的,獨立初始化的變量副本,ThreadLocal實例通常是希望將狀態與線程(例如,用戶ID或事務ID)關聯的類中的私有靜態字段。”
通過老爺子們的描述,指北君大概也知道了ThreadLocal的推薦使用場景,
- ThreadLocal提供了一種訪問某個特有變量的方法 訪問到的變量屬于當前線程,同一線程在任何地方都能訪問同一個線程特有變量。
- 推薦定義為 private static 類型,但是Doug Lea老爺子在ThreadLocalRandom 和 ReentrantReadWriteLock 中使用了 private static final 類型。(肯定是當年寫簡介的時候手抖了)
2.1 Thread中如何存儲
既然是線程的變量,自然是存在Thread對象中的一個變量了,但是它是通過ThreadLocal這個類來維護的。
//與此線程相關的ThreadLocal值,由ThreadLocal這個類維護
ThreadLocal.ThreadLocalMap threadLocals = null;
//與此線程相關的可繼承的ThreadLocal值,由InheritableThreadLocal類來維護
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
ThreadLocal中有一個內部類來ThreadLocalMap來維護這些線程本地變量,
static class ThreadLocalMap {
//初始容量,2的n次方
private static final int INITIAL_CAPACITY = 16;
//根據需要調整數組大小,2的n次方
private Entry[] table;
//上面Entry數組中的元素數量
private int size = 0;
//The next size value at which to resize Default to 0
private int threshold;
}
ThreadLocalMap中的Entry結構如下,是一種key為弱引用(其目的就是Entry對象在GC時容易回收)的hash map,其中key總是ThreadLocal。
static class Entry extends WeakReference< ThreadLocal< ? >> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal< ? > k, Object v) {
super(k);
value = v;
}
}
2.2 常用方法 get,set,remove 詳解
- get() 此方法是ThreadLocal最重要的方法之一,該方法返回此線程局部變量的當前線程副本中的值。大概可分為以下幾步:
(1) 先獲取當前線程,然后再從線程中得到ThreadLocalMap。
(2) 然后使用ThreadLocal對象的threadLocalHashCode進行散列計算,得到一個數組的index
(3) 從Table數組中得到Entry,再對比Entry的key是不是和當前的ThreadLocal相等,如果相等就返回此Entry的value
(4) 如果上一步中得到的Entry與當前ThreadLocal不相等,則會在方法getEntryAfterMiss中進行遍歷Entry數組table中的每一個元素,如果找不到就返回null。而且在遍歷的過程中會順便清理一下廢棄的Entry。
下面可以看一下get方法的具體代碼。
public T get() {
//獲取當前線程
Thread t = Thread.currentThread();
//從當前線程中獲取ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//獲取map中當前ThreadLocal對象對應的entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
private Entry getEntry(ThreadLocal< ? > key) {
//散列計算得到Entry中當前的index
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//如果Entry不是null而且key等于當前
// ThreadLocal對象則返回此Entry
if (e != null && e.get() == key)
return e;
else
//Entry==null 或者其key不等于當前
// ThreadLocal對象,遍歷其余Entry
return getEntryAfterMiss(key, i, e);
}
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)
//如果遍歷過程中發現有Entry的Key為Null,
// 則清除掉作廢的Entry
expungeStaleEntry(i);
else
//計算Entry數組下一個index
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
- set(T value) 此方法將此線程局部變量的當前線程副本中的值設置為指定值。
set線程本地變量步驟如下:
(1) 首先依然是獲取此線程的ThreadLocalMap
(2) Map不為null時往map中插入數據,否側創建map并插入數據
(3) 具體的set方法依然是先遍歷Entry數組中所有的的Entry,然后依次對比每個Entry的key是否等于當前ThreadLocal,如果相等則直接替換現有Entry的value。如果Entry的Key為null,則立馬清理廢棄的Entry,并用新的Entry來替換此卡槽。
(4) 如果遍歷完都沒有return,則在在table中相應卡槽下新建Entry對象
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
private void set(ThreadLocal< ? > key, Object value) {
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();
//如果原Entry的key就是當前ThreadLocal對象,
// 則直接替換現有value
if (k == key) {
e.value = value;
return;
}
if (k == null) {
// 如果Entry的Key為null, 則直接替換為新的Entry
replaceStaleEntry(key, value, i);
return;
}
}
// 如果前面的遍歷沒有return,
// 則插入新的Entry對象到對應的卡槽
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
- remove() remove則相對簡單,直接遍歷ThreadLocalMap中Entry數組table,找到對應的Entry,將Entry的key置為null,然后再清理相應的Entry。
private void remove(ThreadLocal< ? > key) {
...
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
//Entry 的key置為null
e.clear();
// 清理對應卡槽,
expungeStaleEntry(i);
return;
}
}
}
3 Java中使用的ThreadLocal
Java中有哪些源碼使用了ThreadLocal。
ThreadLocalRandom 中使用計算nextGaussian值時有使用到ThreadLocal。
InheritableThreadLocal繼承了ThreadLocal,線程中使用inheritableThreadLocals這個map存儲線程本地變量。和ThreadLocal的區別就是子線程依然可以訪問到父線程的線程本地變量,實際應用中也推薦InheritableThreadLocal
ReentrantReadWriteLock中線程讀寫鎖的計數器使用了ThreadLocal,其目的是記錄每個線程獲取讀寫鎖的次數
static final class ThreadLocalHoldCounter
extends ThreadLocal< HoldCounter > {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
//曾經的Doug Lea老爺子推薦static field,
// 而他默默的使用了static final。
4 如何使用ThreadLocal
ThreadLocal非常適合存儲非線程安全的對象,并且不需要跨線程共享對象。很多需要線程隔離的操作都可以嘗試使用它。
ThreadLocal也非常適合在Web應用程序中使用,典型的應用就是在Web請求進來一開始就將請求狀態存儲在ThreadLocal中,然后參與處理的任何組件均可訪問該狀態。
以下是一個ThreadLocal示例:
具體使用就是配合interceptor或者filter在線程剛開始執行的時候存儲SessionContext,線程執行過程中可以隨時訪問該變量。然后在線程執行結束的時候再調用remove()方法移除,防止內存泄漏。
public class SessionContextHolder {
private static final ThreadLocal< SessionContex > CONTEXHOLDER
= new InheritableThreadLocal< >();
public static void remove(){CONTEXHOLDER.remove();};
public static SessionContex get(){return CONTEXHOLDER.get();}
public static void set(SessionContex sessionContex) {CONTEXHOLDER.set(sessionContex);}
}
總結
本文介紹了ThreadLocal的原理以及解析了常用方法的實現邏輯,以及在ThreadLocal一些應用。在一步步梳理的過程中,果然看到了以往忽略的各種細節,最后給出了一個小Case。并發編程大神道哥.李都在用的ThreadLocal,不妨在自己的項目中偷偷用上,保證絲滑舒適。
-
存儲
+關注
關注
13文章
4263瀏覽量
85671 -
數組
+關注
關注
1文章
415瀏覽量
25908 -
線程
+關注
關注
0文章
504瀏覽量
19651
發布評論請先 登錄
相關推薦
評論