背景
在做一個微服務系統的時候,我們的參數一般都是接在通過方法定義來進行傳遞的,類似這樣
public void xxx(Param p, ...){
// do something
}
然后這時有個模塊,因為之前的設計原因,沒有預留傳遞參數的形式,在本着盡可能不修改原來代碼的情況下,決定通過InhertableThreadLocal
來進行參數傳遞
InhertableThreadLocal
對於InhertableThreadLocal我們不陌生,其實它的思想是以空間來換取線性安全,對每個線程保留一份線程內私有的變量。
這個類一般是用於存在父子線程的情況下,那么在父子線程中,是怎么工作的?結合源碼來簡單認識下
下面這段代碼是從jdk的Thread中摘取的,我們可以看到,每個被創建出來的線程,都有2個threadlocal,分別對應同名的類
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
一開始的時候inheritableThreadLocals
是null的,需要在InhertableThreadLocal調用createMap的時候來初始化。
createMap在setInitialValue()當中會被調用,而setInitialValue被get調用
// ThreadLocal.java
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;
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
// InheritableThreadLocal.java
/**
* Get the map associated with a ThreadLocal.
*
* @param t the current thread
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
/**
* Create the map associated with a ThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the table.
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
一般我們創建InheritableThreadLocal會重寫初始化的方法,類似如下
ThreadLocal<Map<String,Integer>> context = new InheritableThreadLocal<Map<String,Integer>>(){
@Override
protected Map<String,Integer> initialValue() {
System.out.println(Thread.currentThread().getName() + " init value");
return new HashMap<>();
}
};
看到這里估計開始迷糊了,但是只要記住,父子線程的傳遞是通過ThreadLocal.ThreadLocalMap inheritableThreadLocals
這個關鍵的成員變量來實現的。
上面講的其實是父線程怎么創建這個成員變量,那么子線程怎么獲取呢?
從線程池中創建線程,或者普通的創建線程,最終都會調用到這個方法
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
//前面省略
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
// 后面忽略
注意到這個變量了嗎boolean inheritThreadLocals
這個就是決定是否是要繼承父線程中的inheritableThreadLocals
,前提自然是不能為null。
一般的線程new Thread()
這個變量是true,也就是繼承父線程中存放的變量。而線程池,默認使用DefaultThreadFactory
的newThread(Runnable r)
方法,也是如此
到這里就完成了傳遞,解釋了為什么子線程可以得到父線程上set的變量了
回到問題開始
在簡單的介紹完了如何實現變量的傳遞后,我們來看看一開始的問題,測試的代碼如下
@Test
public void ParentChildThread(){
ThreadLocal<Map<String,Integer>> context = new InheritableThreadLocal<Map<String,Integer>>(){
@Override
protected Map<String,Integer> initialValue() {
System.out.println(Thread.currentThread().getName() + " init value");
return new HashMap<>();
}
};
final String TEST_KEY = "tt";
class ChildThread implements Runnable{
@Override
public void run() {
try{
System.out.println(Thread.currentThread().getName());
int a = context.get().get(TEST_KEY);;
System.out.println(a);
}
finally {
// 注意這里
context.remove();
}
}
}
ExecutorService executorService = Executors.newFixedThreadPool(1);
String tname = Thread.currentThread().getName();
int c = 0;
try {
while(c++ < 2) {
System.out.printf("%s ======== %d ========\n", tname, c);
System.out.println(Thread.currentThread().getName() + " set");
// 第一次這里會觸發createMap
// 這里這里存放的是c
context.get().put(TEST_KEY, c);
executorService.execute(new ChildThread());
System.out.println(Thread.currentThread().getName() + " remove");
TimeUnit.MILLISECONDS.sleep(5000L);
context.remove();
}
// 驗證在線程池中remove會不會影響父線程的值,以此來判斷是否需要在父線程中remove
} catch (InterruptedException e) {
e.printStackTrace();
}
}
main線程來模擬spring的線程池,因此需要放在一個循環中,重復的set和remove,子線程來模擬我在多線程環境下獲取參數,因為在線程池中,所以需要記得remove,避免因為線程池復用的關系,而導致參數不對。
讓我們來調試一下,輸出的信息如下
Connected to the target VM, address: '127.0.0.1:46617', transport: 'socket'
main ======== 1 ========
main set
main init value
main remove
pool-1-thread-1
0
main ======== 2 ========
main set
main init value
main remove
pool-1-thread-1
pool-1-thread-1 init value
Exception in thread "pool-1-thread-1" java.lang.NullPointerException
at com.cnc.core.utils.CommonUtilTest$1ChildThread.run(CommonUtilTest.java:43)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
從第一次的使用來看,ok,似乎沒有問題,看第二次,怎么報錯了,比較第一次和第二次,我們發現,因為在子線程中使用了remove,因此第二次需要重新進行初始化pool-1-thread-1 init value
,畢竟我們已經remove了,所以肯定是需要重新初始化的,這個沒有問題
注意到沒這里線程池只有1個線程,這么做的原因是簡化情景,因為實際的情況是32個線程,NPE的錯誤是在一定請求之后發生的
這個錯誤的發生,其實是在復用了之前的線程才出現的,也就是之前線程使用了remove后,就會出現這樣的問題。why?
因為我們InheritableThreadLocal
中存的是map,這個是父線程變量的拷貝
class ChildThread implements Runnable{
@Override
public void run() {
try{
System.out.println(Thread.currentThread().getName());
int a = context.get().get(TEST_KEY);;
System.out.println(a);
}
finally {
// 把這里注釋掉
// context.remove();
}
}
}
注釋上面是保證不再出現異常,我們看看控制台輸出
main ======== 1 ========
main set
main init value
pool-1-thread-1
1
main remove
main ======== 2 ========
main set
main init value
pool-1-thread-1
1
main remove
發現了沒有,輸出的始終是1,我們注意看main線程也有在remove,這其實是切斷了與子線程的聯系
解決措施
根據上面的分析我們知道了,父子線程通過inheritableThreadLocals
來進行變量的共享,根據我們設置的容器是map,其實不需要調用remove,而只要把map的內容清空即可,效果是一樣的,因此,下面這個可以實現我們的需求
context.remove(); --> context.get().clear()
運行測試,,這里我多測試了幾個
main ======== 1 ========
main set
main init value
pool-1-thread-1
1
main remove
main ======== 2 ========
main set
pool-1-thread-1
2
main remove
main ======== 3 ========
main set
pool-1-thread-1
3
main remove
main ======== 4 ========
main set
pool-1-thread-1
4
main remove
main ======== 5 ========
main set
pool-1-thread-1
5
main remove
main ======== 6 ========
main set
pool-1-thread-1
6
main remove