1. 是什么?
首先ThreadLocal
類是一個線程數據綁定類, 有點類似於HashMap<Thread, 你的數據>
(但實際上並非如此), 它所有線程共享, 但讀取其中數據時又只能是獲取線程自己的數據, 寫入也只能給線程自己的數據
2. 怎么用?
public class ThreadLocalDemo {
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
threadLocal.set("zhazha" + Thread.currentThread().getName());
String s = threadLocal.get();
System.out.println("threadName = " + Thread.currentThread().getName() + " [ threadLocal = " + threadLocal + "\t data = " + s + " ]");
}, "threadName" + i).start();
}
}
}
從他的輸入來看, ThreadLocal
是同一個, 數據存的是線程自己的名字, 所以和threadName
是一樣的名稱
threadName = threadName9 [ threadLocal = java.lang.ThreadLocal@43745e1f data = zhazhathreadName9 ]
threadName = threadName3 [ threadLocal = java.lang.ThreadLocal@43745e1f data = zhazhathreadName3 ]
threadName = threadName7 [ threadLocal = java.lang.ThreadLocal@43745e1f data = zhazhathreadName7 ]
threadName = threadName0 [ threadLocal = java.lang.ThreadLocal@43745e1f data = zhazhathreadName0 ]
threadName = threadName6 [ threadLocal = java.lang.ThreadLocal@43745e1f data = zhazhathreadName6 ]
threadName = threadName1 [ threadLocal = java.lang.ThreadLocal@43745e1f data = zhazhathreadName1 ]
threadName = threadName2 [ threadLocal = java.lang.ThreadLocal@43745e1f data = zhazhathreadName2 ]
threadName = threadName4 [ threadLocal = java.lang.ThreadLocal@43745e1f data = zhazhathreadName4 ]
threadName = threadName5 [ threadLocal = java.lang.ThreadLocal@43745e1f data = zhazhathreadName5 ]
threadName = threadName8 [ threadLocal = java.lang.ThreadLocal@43745e1f data = zhazhathreadName8 ]
3. 有什么使用場景
我們使用獲取到一個保存數據庫請求, tomcat會有一個線程去操作數據庫保存數據和響應數據給客戶, 而操作數據庫需要存在一個數據庫鏈接Connection
對象, 只要是同一個數據庫鏈接, 就可以得到同一個事務
但一個線程是如何獲取同一個Connection
從而獲取同一個事務 ?
方法其實很簡單, 使用 ThreadLocal
綁定在線程中, 類似於Map<Thread, Connection>
去存儲
4. 底層源碼分析
get
方法分析
public T get() {
// 獲取當前線程
Thread t = Thread.currentThread();
// 獲取ThreadLocalMap
ThreadLocal.ThreadLocalMap map = getMap(t);
// map不為null
if (map != null) {
// 根據this獲取我們的entry
ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果map獲取為空, 則初始化
return setInitialValue();
}
根據上面源碼分析發現ThreadLocal
底層使用的不是類似Map<Thread, Data>
這種結構而是
每個線程都有一個屬於自己的ThreadLocalMap
結構
而他的結構是這樣的
其中的table
數組在上面的 setInitialValue()
方法創建詳細源碼在這
private T setInitialValue() {
// 這個方法在我們的用例中沒寫, 所以默認放回 null
T value = initialValue();
Thread t = Thread.currentThread();
// 獲取線程單獨的 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 如果我們初始化了initialValue() 方法, 那么它默認初始化的值會被設置到這里,
// 但是實際上我們用例為null, 所以不會執行這段代碼
map.set(this, value);
} else {
// 線程ThreadLocalMap 沒被創建, 需要創建出來,
// 其中的 table 數組在這里被創建
createMap(t, value);
}
// 這里我沒分析, 忽略了
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
他會在ThreadLocalMap
中調用構造方法初始化
// 其中 firstValue是我們的值
void createMap(Thread t, T firstValue) {
// 關注下 this , 它是ThreadLocal
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 我們的table在這里被創建, INITIAL_CAPACITY == 16
table = new Entry[INITIAL_CAPACITY];
// 獲取不超過16的hashCode
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 根據計算出來的HashCode設置到對應的table數組中, 這里key是ThreadLocal, value是我們的值
table[i] = new Entry(firstKey, firstValue);
// 初始時, 已經有一個值了, 所以size = 1
size = 1;
// 設置擴容閾值加載因子 threshold = len * 2 / 3; 默認為長度的三分之二
setThreshold(INITIAL_CAPACITY);
}
從這段代碼可以發現, firstKey
其實是我們ThreadLocalMap
中的key
, 而firstKey
就是我們的ThreadLocal
, 而value
就是我們 initialValue()
方法返回的值, 這里默認為null
, 所以我們可以得出這樣一幅圖
總結下
每個線程都有一個屬於自己的ThreadLocalMap
類, 他用於關聯多個以ThreadLocal
對象為key
, 以你的數據
為value
的Entry
對象, 且該對象的key
是一個弱引用對象
接下來我們分析下這個類Entry
, 它繼承了弱引用類WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
// ThreadLocal被設置為弱引用
super(k);
// 保存value
value = v;
}
}
發現 ThreadLocal
被設置為弱引用
存在什么問題?
為什么前面的Entry
需要繼承弱引用類WeakReference
呢?
首先了解下什么是引用
簡單了解下強、軟、弱和虛引用
- 強引用: 如果引用變量沒被指向null則, 引用對象將被停留在堆中, 無法被虛擬機回收
Object obj = new Object()
- 軟引用: 如果虛擬機堆內存不夠用了(在發生內存溢出之前), 虛擬機可以選擇回收軟引用對象, 虛擬機提供
SoftReference
類實現軟引用, 一般用於相對比較重要但又可以不用的對象, 比如: 緩存 - 弱引用: 生於系統回收之前, 死於系統回收完畢之后, 弱引用需要依附於強引用或者軟引用才能夠防止被虛擬機回收, 比如放到一個引用隊列(
ReferenceQueue
)中或者對象中, 比如:ThreadLocalMap
的Entry
對象, 需要依附於ThreadLocal
才能夠不被刪除掉 - 虛引用: 可以理解為跟強引用對象沒了引用變量一樣, 隨時可以被回收, 只要依附於引用隊列中才不會被回收, 通常用於網絡通訊的
NIO
上, 用於引用直接內存, java提供類PhantomReference
來實現虛引用
為何Entry
對象需要為弱引用?
答案很明顯, 防止內存泄漏[1], 我們來詳細分析分析
首先, 我們知道ThreadLocalMap
中存放的是一個一個Entry
對象, 而 Entry
對象中的key
(ThreadLocal
)被設計成弱引用如果key
被設置成null
(比如: 外部的測試用例中的private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
這個對象被設置為 threadLocal = null
) 則, 你會發現此時Entry
的對象key = null value = xxxx
(此時這個Entry
實質上是沒有用的, 連key
都給設置成null
, 它的value
還有什么用?) 而ThreadLocalMap
中存儲的還是Entry
對象的地址, 此Entry
不會被回收, 但Entry
對象的key
被設置成弱引用, 就不一樣了, 直接會被回收掉它
[1]內存泄漏: 程序中己動態分配的堆內存由於某種原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等嚴重后果
那么這樣就沒有問題了么???(打臉篇)
再次強調, 下面這段話別信, 仔細看到最后, 你會發現這被打臉了
其實應該是沒什么問題了(被自己打臉了, 別信這句話), 只不過很多網友覺得Entry
中的key
雖然是弱引用, 但Entry
可能不會被回收, 因為entry
的value
是強引用, 可能導致線程下的entry
無法被回收掉, 最好推薦使用threadLocal.remove
方法刪除掉, 前面說的threadLocal = null
方法不推薦使用, 那么為了以防萬一吧, 還是手動調用下remove
方法比較好一點
下面是我對threadLocal = null
方式的代碼測試:
public class ThreadLocalDemo {
private static ThreadLocal<String> threadLocal1 = new ThreadLocal<>() {
@Override
protected String initialValue() {
return "1";
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("threadLocal1被回收");
}
};
private static ThreadLocal<String> threadLocal2 = new ThreadLocal<>() {
@Override
protected String initialValue() {
return "2";
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("threadLocal1被回收");
}
};
public static void main(String[] args) throws InterruptedException, NoSuchFieldException, IllegalAccessException {
// 獲取ThreadLocalMap
Thread thread = Thread.currentThread();
Class<? extends Thread> clazz = thread.getClass();
Field threadLocals = clazz.getDeclaredField("threadLocals");
threadLocals.setAccessible(true);
Object threadLocalsObj = threadLocals.get(thread);
// 獲取ThreadLocalMap下的table數組
Class<?> threadLocalsMapClass = threadLocalsObj.getClass();
Field tableField = threadLocalsMapClass.getDeclaredField("table");
tableField.setAccessible(true);
Object[] tableObj = (Object[]) tableField.get(threadLocalsObj);
threadLocal1.set("zhazha");
threadLocal2.set("xixi");
System.out.println(threadLocal1.get());
System.out.println(threadLocal2.get());
// 在這里下一個斷點看看ThreadLocal被回收, Entry是否被回收
threadLocal1 = null;
threadLocal2 = null;
System.gc();
Thread.sleep(5000);
System.out.println(tableObj);
System.out.println("主線程結束");
}
}
輸出是這樣的:
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.zhazha.threadlocal.ThreadLocalDemo (file:/D:/program/codes/java/Concurrentcy/reviewjuc/target/classes/) to field java.lang.Thread.threadLocals
WARNING: Please consider reporting this to the maintainers of com.zhazha.threadlocal.ThreadLocalDemo
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
zhazha
xixi
[Ljava.lang.ThreadLocal$ThreadLocalMap$Entry;@aecb35a
主線程結束
如果上面的代碼不調用gc
方法, 很長一段時間內不會被回收, 應該是jvm gc
還沒開始被動回收
但!!!但!!!但!!! 看調試代碼
數組中的referent
字段還是存在的, 下圖是gc
回收之前查看數組中的元素發現, 字段referent
(也就是ThreadLocal
) 它還在
在gc
方法執行完畢后, referent
被回收掉了, referent = null
了
但是那個對象怎么回事??? 沒被回收掉?? 打臉了??? 求助廣大網友給我看看
那讓我們試試 remove
方法試試?
好了, 直接沒了, 找不到那兩個屬性了
這An illegal reflective access operation has occurred
這個問題怎么幫? 這回真不知道了, 應該不影響我們的代碼么?
算了為了把這個紅色的字改沒掉, 改了改源碼
public class ThreadLocalDemo {
private static ThreadLocal<String> threadLocal1 = new ThreadLocal<>() {
@Override
protected String initialValue() {
return "1";
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("threadLocal1被回收");
}
};
private static ThreadLocal<String> threadLocal2 = new ThreadLocal<>() {
@Override
protected String initialValue() {
return "2";
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("threadLocal1被回收");
}
};
private static Unsafe unsafe;
static {
Class<Unsafe> unsafeClass = Unsafe.class;
Unsafe unsafe = null;
try {
Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
ThreadLocalDemo.unsafe = (Unsafe) unsafeField.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException, NoSuchFieldException {
Thread thread = Thread.currentThread();
long threadLocalsFieldOffset = unsafe.objectFieldOffset(Thread.class.getDeclaredField("threadLocals"));
Object threadLocalMapObj = unsafe.getObject(thread, threadLocalsFieldOffset);
long tableOffset = unsafe.objectFieldOffset(threadLocalMapObj.getClass().getDeclaredField("table"));
Object tableObj = unsafe.getObject(threadLocalMapObj, tableOffset);
threadLocal1.set("zhazha");
threadLocal2.set("xixi");
System.out.println(threadLocal1.get());
System.out.println(threadLocal2.get());
threadLocal1 = null;
threadLocal2 = null;
// threadLocal1.remove();
// threadLocal2.remove();
System.gc();
System.out.println(tableObj);
System.out.println("主線程結束");
}
}
好了沒這個問題了
zhazha
xixi
threadLocal1被回收
threadLocal1被回收
[Ljava.lang.ThreadLocal$ThreadLocalMap$Entry;@7dc222ae
主線程結束
與目標VM斷開連接, 地址為: ''127.0.0.1:58958',傳輸: '套接字'', 傳輸: '{1}'
進程已結束,退出代碼0