前言
在實際的工作中蕉堰,有些時候我們會遇到一些線程之間共享數(shù)據(jù)的問題旅掂,比如一下幾個典型的業(yè)務場景,舉幾個例子什荣,大家能夠更加直觀的感受到
-
分布式鏈路追蹤系統(tǒng)
這個不必多說矾缓,如果我們需要記錄系統(tǒng)的每個調用鏈路,那么每一個子系統(tǒng)里面稻爬,如果調用了異步線程來做處理的話嗜闻,那么類似這種鏈路是不是需要收集起來呢?
-
日志收集記錄系統(tǒng)上下文
在實際的日志打印記錄中桅锄,一個http請求進來的話琉雳,每一行日志,日志產(chǎn)生的線程信息竞滓?上下文信息咐吼,是不是需要記錄下來呢?
上面我舉的是我們最最最常見的兩個例子了商佑, 做了幾年開發(fā)的都能理解為啥要有這個東西锯茄,下面我們仔細聊一下這個問題,
InheritableThreadLocal
其實說到這個問題茶没,有些同學就會想到InheritableThreadLocal
這個工具了肌幽,這是JDK給我們提供的的工具,該工具可以解決父子線程之間的值的傳遞抓半,我們先來一個簡單的demo, 然后再進行原理分析
demo
/**
* ce
*
* @author zhangyunhe
* @date 2020-04-22 16:19
*/
public class InteritableTest2 {
static ThreadLocal<String> local = new InheritableThreadLocal<>();
// 初始化一個長度為1 的線程池
static ExecutorService poolExecutor = Executors.newFixedThreadPool(1);
public static void main(String[] args) throws ExecutionException, InterruptedException {
InteritableTest2 test = new InteritableTest2();
test.test();
}
private void test(){
// 設置一個初始值
local.set("天王老子");
poolExecutor.submit(new Task());
}
class Task implements Runnable{
@Override
public void run() {
// 子線程里面打印獲取到的值
System.out.println(Thread.currentThread().getName()+":"+local.get());
}
}
}
輸出結果
pool-1-thread-1:天王老子
從上面可以看到喂急, 子線程pool-1-thread-1可以獲取到父線程在local里面設置的值,這就實現(xiàn)了值的傳遞了笛求。
源碼分析
下面我們從源碼的角度上看一下InheritableThreadLocal
的實現(xiàn)廊移,他究竟是怎么做到父子線程之間線程的傳遞的糕簿。
我們首先看一下Thread創(chuàng)建的代碼。
Thread
線程初始化的代碼狡孔,可以看到重點在init方法
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
init
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
// 1. 獲取當前線程為父線程懂诗,其實就是創(chuàng)建這個線程的線程
Thread parent = currentThread();
// 省略代碼。苗膝。殃恒。。辱揭。
// 2. 判斷inheritThreadLocals 是否==true离唐, 父節(jié)點的inheritableThreadLocals是否不為空
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
//3. 符合以上的話,那么創(chuàng)建當前線程的inheritableThreadLocals
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
}
步驟說明:
看著我上面標的步驟進行說明
1.獲取當前線程為父線程问窃,其實就是創(chuàng)建這個線程的線程
2.判斷inheritThreadLocals 是否==true 亥鬓, 默認inheritThreadLocals就是為true, 通用的new Thread()方法域庇,這個值就是true, 同時判斷父節(jié)點的inheritableThreadLocals是否為空贮竟, 如果不為空,則說明需要進行傳遞较剃。
3.在這個if里面咕别,針對當前線程做了inheritableThreadLocals的初始化, 把父線程的值拷貝到這個里面來写穴。
通過上面的分析惰拱,其實基本的原理都已經(jīng)了解清楚了,不熟悉的可以可以自己去細細研究啊送。
那么是否這種做法完全可以符合我們的需求呢偿短? 我們看一下下面的場景
線程池異常場景
線程池demo
/**
* ce
*
* @author zhangyunhe
* @date 2020-04-22 16:19
*/
public class InteritableTest {
static ThreadLocal<String> local = new InheritableThreadLocal<>();
static ExecutorService poolExecutor = Executors.newFixedThreadPool(1);
public static void main(String[] args) throws ExecutionException, InterruptedException {
InteritableTest test = new InteritableTest();
test.test();
}
private void test() throws ExecutionException, InterruptedException {
local.set("天王老子");
Future future = poolExecutor.submit(new Task("任務1"));
future.get();
Future future2 = poolExecutor.submit(new Task("任務2"));
future2.get();
System.out.println("父線程的值:"+local.get());
}
class Task implements Runnable{
String str;
Task(String str){
this.str = str;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":"+local.get());
local.set(str);
System.out.println(local.get());
}
}
}
輸出結果:
pool-1-thread-1:天王老子
任務1
pool-1-thread-1:任務1
任務2
父線程的值:天王老子
從上面可以看到,Task2執(zhí)行的時候馋没,獲取到的父線程的值是Task1修改過的昔逗。 這樣感覺是不是就破壞了我們的本意? 實際上篷朵,這是因為我們使用了線程池勾怒,在池化技術里面,線程是會被復用的声旺,當執(zhí)行Task2的時候笔链,實際上是用的Task1的那個線程,那個線程已經(jīng)被創(chuàng)建好了的腮猖,所以那里面的locals就是被Task1修改過的鉴扫,那么遇到這種問題,該如何解決呢澈缺?
下一篇文章給大家介紹一個組件坪创,在使用線程池等會池化復用線程的執(zhí)行組件情況下炕婶,提供ThreadLocal值的傳遞功能