我們知道有時(shí)候一個(gè)對(duì)象的共享變量會(huì)被多個(gè)線程所訪問(wèn)涯呻,這時(shí)就會(huì)有線程安全問(wèn)題。當(dāng)然我們可以使用synchorinized 關(guān)鍵字來(lái)為此變量加鎖叉趣,進(jìn)行同步處理胜臊。從而限制只能有一個(gè)線程來(lái)使用此變量,但是加鎖會(huì)大大影響程序執(zhí)行效率裙犹,此外我們還可以使用ThreadLocal來(lái)解決對(duì)某一個(gè)變量的訪問(wèn)沖突問(wèn)題尽狠。
一、ThreadLocal 概述
當(dāng)使用ThreadLocal維護(hù)變量的時(shí)候 它為每一個(gè)使用該變量的線程提供一個(gè)獨(dú)立的變量副本叶圃,即每個(gè)線程內(nèi)部都會(huì)有一個(gè)該變量晚唇,這樣同時(shí)多個(gè)線程訪問(wèn)該變量并不會(huì)彼此相互影響,因此他們使用的都是自己從內(nèi)存中拷貝過(guò)來(lái)的變量的副本盗似, 這樣就不存在線程安全問(wèn)題哩陕,也不會(huì)影響程序的執(zhí)行性能。
ThreadLocal 的幾個(gè)方法: ThreadLocal 可以存儲(chǔ)任何類型的變量對(duì)象赫舒, get返回的是一個(gè)Object對(duì)象悍及,但是我們可以通過(guò)泛型來(lái)制定存儲(chǔ)對(duì)象的類型。
public T get() { } // 用來(lái)獲取ThreadLocal在當(dāng)前線程中保存的變量副本
public void set(T value) { } //set()用來(lái)設(shè)置當(dāng)前線程中變量的副本
public void remove() { } //remove()用來(lái)移除當(dāng)前線程中變量的副本
protected T initialValue() { } //initialValue()是一個(gè)protected方法接癌,一般是用來(lái)在使用時(shí)進(jìn)行重寫(xiě)的
Thread 在內(nèi)部是通過(guò)ThreadLocalMap來(lái)維護(hù)ThreadLocal變量表心赶, 在Thread類中有一個(gè)threadLocals 變量,是ThreadLocalMap類型的缺猛,它就是為每一個(gè)線程來(lái)存儲(chǔ)自身的ThreadLocal變量的缨叫, ThreadLocalMap是ThreadLocal類的一個(gè)內(nèi)部類,這個(gè)Map里面的最小的存儲(chǔ)單位是一個(gè)Entry荔燎, 它使用ThreadLocal作為key耻姥, 變量作為 value,這是因?yàn)樵诿恳粋€(gè)線程里面有咨,可能存在著多個(gè)ThreadLocal變量
初始時(shí)琐簇,在Thread里面,threadLocals為空座享,當(dāng)通過(guò)ThreadLocal變量調(diào)用get()方法或者set()方法婉商,就會(huì)對(duì)Thread類中的threadLocals進(jìn)行初始化似忧,并且以當(dāng)前ThreadLocal變量為鍵值,以ThreadLocal要保存的副本變量為value丈秩,存到threadLocals盯捌。
然后在當(dāng)前線程里面,如果要使用副本變量蘑秽,就可以通過(guò)get方法在threadLocals里面查找
我們來(lái)看一個(gè)使用示例:
public class Test {
ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
ThreadLocal<String> stringLocal = new ThreadLocal<String>();
public void set() {
longLocal.set(Thread.currentThread().getId());
stringLocal.set(Thread.currentThread().getName());
}
public long getLong() {
return longLocal.get();
}
public String getString() {
return stringLocal.get();
}
public static void main(String[] args) throws InterruptedException {
final Test test = new Test();
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
Thread thread1 = new Thread(){
public void run() {
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
};
};
thread1.start();
thread1.join();
System.out.println(test.getLong());
System.out.println(test.getString());
}
}
輸出結(jié)果為
1
main
9
Thread-0
1
main
二挽唉、父子線程傳遞InheritableThreadLocal
以上方案在父子線程中就有了局限性,如果子線程想要拿到父線程的中的ThreadLocal值怎么辦呢筷狼?比如會(huì)有以下的這種代碼的實(shí)現(xiàn)。由于ThreadLocal的實(shí)現(xiàn)機(jī)制,在子線程中g(shù)et時(shí),我們拿到的Thread對(duì)象是當(dāng)前子線程對(duì)象,那么他的ThreadLocalMap是null的,所以我們得到的value也是null匠童。
final ThreadLocal threadLocal=new ThreadLocal(){
@Override
protected Object initialValue() {
return "xiezhaodong";
}
};
new Thread(new Runnable() {
@Override
public void run() {
threadLocal.get();//NULL
}
}).start();
JDK已經(jīng)為這種情況提供了實(shí)現(xiàn)方案:InheritableThreadLocal埂材。大致的解釋了一下InheritableThreadLocal為什么能解決父子線程傳遞Threadlcoal值的問(wèn)題。
1)在創(chuàng)建InheritableThreadLocal對(duì)象的時(shí)候賦值給線程的t.inheritableThreadLocals變量
2)在創(chuàng)建新線程的時(shí)候會(huì)check父線程中t.inheritableThreadLocals變量是否為null汤求,如果不為null則copy一份ThradLocalMap到子線程的t.inheritableThreadLocals成員變量中去
3)因?yàn)閺?fù)寫(xiě)了getMap(Thread)和CreateMap()方法,所以get值得時(shí)候俏险,就可以在getMap(t)的時(shí)候就會(huì)從t.inheritableThreadLocals中拿到map對(duì)象,從而實(shí)現(xiàn)了可以拿到父線程ThreadLocal中的值
所以,在最開(kāi)始的代碼示例中扬绪,如果把ThreadLocal對(duì)象換成InheritableThreadLocal對(duì)象竖独,那么get到的字符會(huì)是“xiezhaodong”而不是NULL
二、線程池傳遞TransmittableThreadLocal
我們?cè)谑褂镁€程的時(shí)候往往不會(huì)只是簡(jiǎn)單的new Thrad對(duì)象挤牛,而是使用線程池莹痢,當(dāng)然線程池的好處多多。這里不詳解墓赴,既然這里提出了問(wèn)題竞膳,那么線程池會(huì)給InheritableThreadLocal帶來(lái)什么問(wèn)題呢?我們列舉一下線程池的特點(diǎn):
1)為了減小創(chuàng)建線程的開(kāi)銷诫硕,線程池會(huì)緩存已經(jīng)使用過(guò)的線程
2)生命周期統(tǒng)一管理,合理的分配系統(tǒng)資源
對(duì)于第一點(diǎn)坦辟,如果一個(gè)子線程已經(jīng)使用過(guò),并且會(huì)set新的值到ThreadLocal中章办,那么第二個(gè)task提交進(jìn)來(lái)的時(shí)候還能獲得父線程中的值嗎锉走?答案是不能,如果我們能夠藕届,在使用完這個(gè)線程的時(shí)候清除所有的localMap挪蹭,在submit新任務(wù)的時(shí)候在重新重父線程中copy所有的Entry。然后重新給當(dāng)前線程的t.inhertableThreadLocal賦值休偶。這樣就能夠解決在線程池中每一個(gè)新的任務(wù)都能夠獲得父線程中ThreadLocal中的值而不受其他任務(wù)的影響嚣潜,因?yàn)樵谏芷谕瓿傻臅r(shí)候會(huì)自動(dòng)clear所有的數(shù)據(jù)。Alibaba的一個(gè)庫(kù)解決了這個(gè)問(wèn)題github:alibaba/transmittable-thread-local
如何使用
這個(gè)庫(kù)最簡(jiǎn)單的方式是這樣使用的,通過(guò)簡(jiǎn)單的修飾椅贱,使得提交的runable擁有了上一節(jié)所述的功能懂算。具體的API文檔詳見(jiàn)github只冻,這里不再贅述
TransmittableThreadLocal<String> parent = new TransmittableThreadLocal<String>();
parent.set("value-set-in-parent");
Runnable task = new Task("1");
// 額外的處理,生成修飾了的對(duì)象ttlRunnable
Runnable ttlRunnable = TtlRunnable.get(task);
executorService.submit(ttlRunnable);
// Task中可以讀取, 值是"value-set-in-parent"
String value = parent.get();
原理簡(jiǎn)述
這個(gè)方法TtlRunnable.get(task)最終會(huì)調(diào)用構(gòu)造方法计技,返回的是該類本身喜德,也是一個(gè)Runable,這樣就完成了簡(jiǎn)單的裝飾。最重要的是在run方法這個(gè)地方垮媒。
public final class TtlRunnable implements Runnable {
private final AtomicReference<Map<TransmittableThreadLocal<?>, Object>> copiedRef;
private final Runnable runnable;
private final boolean releaseTtlValueReferenceAfterRun;
private TtlRunnable(Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
//從父類copy值到本類當(dāng)中
this.copiedRef = new AtomicReference<Map<TransmittableThreadLocal<?>, Object>>(TransmittableThreadLocal.copy());
this.runnable = runnable;//提交的runable,被修飾對(duì)象
this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
}
/**
* wrap method {@link Runnable#run()}.
*/
@Override
public void run() {
Map<TransmittableThreadLocal<?>, Object> copied = copiedRef.get();
if (copied == null || releaseTtlValueReferenceAfterRun && !copiedRef.compareAndSet(copied, null)) {
throw new IllegalStateException("TTL value reference is released after run!");
}
//裝載到當(dāng)前線程
Map<TransmittableThreadLocal<?>, Object> backup = TransmittableThreadLocal.backupAndSetToCopied(copied);
try {
runnable.run();//執(zhí)行提交的task
} finally {
//clear
TransmittableThreadLocal.restoreBackup(backup);
}
}
}
在上面的使用線程池的例子當(dāng)中舍悯,如果換成這種修飾的方式進(jìn)行操作,B任務(wù)得到的肯定是父線程中ThreadLocal的值睡雇,解決了在線程池中InheritableThreadLocal不能解決的問(wèn)題萌衬。
如何更新父線程ThreadLocal值?
如果線程之間出了要能夠得到父線程中的值它抱,同時(shí)想更新值怎么辦呢秕豫?在前面我們有提到,當(dāng)子線程copy父線程的ThreadLocalMap的時(shí)候是淺拷貝的,代表子線程Entry里面的value都是指向的同一個(gè)引用观蓄,我們只要修改這個(gè)引用的同時(shí)就能夠修改父線程當(dāng)中的值了,比如這樣:
@Override
public void run() {
System.out.println("========");
Span span= inheritableThreadLocal.get();
System.out.println(span);
span.name="liuliuliu";//修改父引用為liuliuliu
inheritableThreadLocal.set(new Span("zhangzhangzhang"));
System.out.println(inheritableThreadLocal.get());
}
這樣父線程中的值就會(huì)得到更新了混移。能夠滿足父線程ThreadLocal值的實(shí)時(shí)更新,同時(shí)子線程也能共享父線程的值侮穿。不過(guò)場(chǎng)景倒是不是很常見(jiàn)的樣子歌径。
參考文章
本文作者: catalinaLi
本文鏈接: http://catalinali.top/2018/helloThreadLocal/