1.ThreadLocal
在分析問題之前我們先來看一下ThreadLocal的內部獲取數據的方法:
可以看到160行代碼,獲取了當前線程。並且通過getMap方法傳入了當前線程,並返回了ThreadLocalMap。然后轉為Entry類型,再取出相應的值。
而getMap方法實現如下:
看到getMap方法的實現我們可以知道,其實每個線程都是維護了一個ThreadLocalMap,而ThreadLocalMap是ThreadLocal的一個內部類。
筆者在閱讀Thread類源碼的時候發現Thread本身並沒有對ThreadLocal.ThreadLocalMap進行初始化。而是在使用ThreadLocal類Set方法、Get方法的時候完成初始化的。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
由於是線程級的變量,所以在線程之間肯定是無法共享的,如下例代碼:
package thread.pdd;
/**
* @author:liyangpeng
* @date:2020/5/25 11:38
*/
public class ThreadLocalTest {
public static void main(String[] args) {
ThreadLocal<String> threadLocal=new ThreadLocal<>();
threadLocal.set("1111");
Thread thread=new Thread(()->{
System.out.println(threadLocal.get());
});
thread.start();
}
}
輸出結果:null
相信認真閱讀本文的讀者肯定注意到了圖3 對Thread類的代碼截圖。我們只講了命名為 threadLocals 的ThreadLocal.ThreadLocalMap,他還有一個命名為 inheritableThreadLocals 的 ThreadLocal.ThreadLocalMap。沒錯,就是他了。
Thread類並沒有對 threadLocals 變量進行初始化操作。但是我們可以繼續閱讀源碼:Thread 的默認構造調用了 init 方法。init方法內部對inheritableThreadLocals 變量進行了初始化。
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;
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
//線程組加入邏輯
if (g == null) {
//獲取到安全管理器則使用安全管理器
if (security != null) {
g = security.getThreadGroup();
}
//未獲取到安全管理器則加入父線程線程組
if (g == null) {
g = parent.getThreadGroup();
}
}
g.checkAccess();
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
//是否守護線程
this.daemon = parent.isDaemon();
//線程權重
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
//【注意此處代碼實現】如果父線程inheritableThreadLocals 不為null,則初始化自身inheritThreadLocals。
// ThreadLocal.createInheritedMap方法傳入了父線程的inheritableThreadLocals
// 返回的是 new ThreadLocalMap(parentMap),注意此處是引用傳遞。理論上也就是說子線程修改數據對父線程是可見的。
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
//分配線程內存
this.stackSize = stackSize;
//設置線程id
tid = nextThreadID();
}
當然了 ThreadLocal 內部是沒有對線程的 inheritableThreadLocals進行讀寫操作的。好了,不跟你多BB,馬上進入我們的第二塊內容。
2.InheritableThreadLocal
大家點開源碼可以看到InheritableThreadLocal是繼承自ThreadLocal的。主要是覆蓋了ThreadLocal的以下幾個方法:
大家看到上面ThreadLocal的 get、set方法可以知道,如果是要讀寫數據的時候首先要調用的則是getMap方法,如果當前沒有實例化ThreadLocalMap則先實例化,覆蓋createMap方法直接為線程的inheritableThreadLocals屬性完成初始化。
InheritableThreadLocal類的getMap方法返回的則是當前線程的inheritableThreadLocals變量。
包括上面的實例化大家也能知道子線程初始化的時候inheritableThreadLocals變量是直接從父線程獲取的。由此便可以完成父子線程ThreadLocal數據共享啦。下面請查看代碼案例:
package thread.pdd;
import java.util.stream.IntStream;
/**
* @author:liyangpeng
* @date:2020/5/25 11:38
*/
public class ThreadLocalTest {
public static void main(String[] args) throws InterruptedException {
InheritableThreadLocal<String> threadLocal=new InheritableThreadLocal<>();
threadLocal.set("1111");
Thread thread=new Thread(()->{
System.out.println(threadLocal.get());
});
thread.start();
Thread.sleep(500);
System.out.println("-------------------華麗的分割線-----------------");
//流操作|內部實現也是多線程
IntStream.range(0,10).parallel().forEach(id->{
System.out.println(id+"_~_~_"+threadLocal.get());
});
}
}
輸出結果:
1111
-------------------華麗的分割線-----------------
6_~_~_1111
5_~_~_1111
1_~_~_1111
0_~_~_1111
4_~_~_1111
8_~_~_1111
3_~_~_1111
9_~_~_1111
7_~_~_1111
2_~_~_1111
於是乎,父子線程ThreadLocal數據共享我們便完成了,你以為這樣就結束了嗎?不。遠遠沒有你想的這么簡單。我們將以上的代碼稍作修改:
package thread.pdd;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;
/**
* @author:liyangpeng
* @date:2020/5/25 11:38
*/
public class ThreadLocalTest {
public static void main(String[] args){
InheritableThreadLocal<String> threadLocal=new InheritableThreadLocal<>();
threadLocal.set("--->哈哈哈");
ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.execute(()->{
Thread thread=Thread.currentThread();
System.out.println(thread.getName()+"__"+threadLocal.get());
});
//重新修改InheritableThreadLocal內的數據
threadLocal.set("--->呵呵呵");
executorService.execute(()->{
Thread thread=Thread.currentThread();
System.out.println(thread.getName()+"__"+threadLocal.get());
});
executorService.shutdown();
}
}
//猜猜以上結果是啥?是 哈哈哈 然后 呵呵呵?
//不不不,結果出人意料的是:
//pool-1-thread-1__--->哈哈哈
//pool-1-thread-1__--->哈哈哈
是以上代碼對 threadLocal 的修改沒有成功嗎?不,其實修改是成功了,但是由於線程池為了減少線程創建的開支,對使用完畢的線程並沒有立即銷毀。而是繼續返還到了線程池中,所以我們下次使用線程的時候。並不會重新創建一個新的線程,也就是不會執行線程初始化的init方法。也有同學會問。之前初始化InheritableThreadLocal的時候不是引用傳遞嗎?為什么修改不了?因為你沒有詳細看ThreadLocalMap的創建過程源碼。ThreadLocalMap只是復制了一份新的數據,並沒有直接使用父線程的InheritableThreadLocal。
出自程序猿本性,出了問題一定要去解決問題。那我們怎么去解決這個問題呢?不用緊張,已經有人幫我們解決了這個問題。
阿里的 transmittable-thread-local 已經幫我們解決了這個問題。
使用方式以及實現原理想知道的同學可以自己去研究。
熱愛編程的朋友可以加入QQ技術交流群:1026659175