我們知道有時候一個對象的共享變量會被多個線程所訪問,這時就會有線程安全問題。當然我們可以使用synchorinized 關鍵字來為此變量加鎖,進行同步處理。從而限制只能有一個線程來使用此變量,但是加鎖會大大影響程序執行效率,此外我們還可以使用ThreadLocal來解決對某一個變量的訪問沖突問題。
一、ThreadLocal 概述
當使用ThreadLocal維護變量的時候 它為每一個使用該變量的線程提供一個獨立的變量副本,即每個線程內部都會有一個該變量,這樣同時多個線程訪問該變量并不會彼此相互影響,因此他們使用的都是自己從內存中拷貝過來的變量的副本, 這樣就不存在線程安全問題,也不會影響程序的執行性能。
ThreadLocal 的幾個方法: ThreadLocal 可以存儲任何類型的變量對象, get返回的是一個Object對象,但是我們可以通過泛型來制定存儲對象的類型。
public T get() { } // 用來獲取ThreadLocal在當前線程中保存的變量副本
public void set(T value) { } //set()用來設置當前線程中變量的副本
public void remove() { } //remove()用來移除當前線程中變量的副本
protected T initialValue() { } //initialValue()是一個protected方法,一般是用來在使用時進行重寫的
Thread 在內部是通過ThreadLocalMap來維護ThreadLocal變量表, 在Thread類中有一個threadLocals 變量,是ThreadLocalMap類型的,它就是為每一個線程來存儲自身的ThreadLocal變量的, ThreadLocalMap是ThreadLocal類的一個內部類,這個Map里面的最小的存儲單位是一個Entry, 它使用ThreadLocal作為key, 變量作為 value,這是因為在每一個線程里面,可能存在著多個ThreadLocal變量
初始時,在Thread里面,threadLocals為空,當通過ThreadLocal變量調用get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,并且以當前ThreadLocal變量為鍵值,以ThreadLocal要保存的副本變量為value,存到threadLocals。
然后在當前線程里面,如果要使用副本變量,就可以通過get方法在threadLocals里面查找
我們來看一個使用示例:
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());
}
}
輸出結果為
1
main
9
Thread-0
1
main
二、父子線程傳遞InheritableThreadLocal
以上方案在父子線程中就有了局限性,如果子線程想要拿到父線程的中的ThreadLocal值怎么辦呢?比如會有以下的這種代碼的實現。由于ThreadLocal的實現機制,在子線程中get時,我們拿到的Thread對象是當前子線程對象,那么他的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已經為這種情況提供了實現方案:InheritableThreadLocal。大致的解釋了一下InheritableThreadLocal為什么能解決父子線程傳遞Threadlcoal值的問題。
1)在創建InheritableThreadLocal對象的時候賦值給線程的t.inheritableThreadLocals變量
2)在創建新線程的時候會check父線程中t.inheritableThreadLocals變量是否為null,如果不為null則copy一份ThradLocalMap到子線程的t.inheritableThreadLocals成員變量中去
3)因為復寫了getMap(Thread)和CreateMap()方法,所以get值得時候,就可以在getMap(t)的時候就會從t.inheritableThreadLocals中拿到map對象,從而實現了可以拿到父線程ThreadLocal中的值
所以,在最開始的代碼示例中,如果把ThreadLocal對象換成InheritableThreadLocal對象,那么get到的字符會是“xiezhaodong”而不是NULL
二、線程池傳遞TransmittableThreadLocal
我們在使用線程的時候往往不會只是簡單的new Thrad對象,而是使用線程池,當然線程池的好處多多。這里不詳解,既然這里提出了問題,那么線程池會給InheritableThreadLocal帶來什么問題呢?我們列舉一下線程池的特點:
1)為了減小創建線程的開銷,線程池會緩存已經使用過的線程
2)生命周期統一管理,合理的分配系統資源
對于第一點,如果一個子線程已經使用過,并且會set新的值到ThreadLocal中,那么第二個task提交進來的時候還能獲得父線程中的值嗎?答案是不能,如果我們能夠,在使用完這個線程的時候清除所有的localMap,在submit新任務的時候在重新重父線程中copy所有的Entry。然后重新給當前線程的t.inhertableThreadLocal賦值。這樣就能夠解決在線程池中每一個新的任務都能夠獲得父線程中ThreadLocal中的值而不受其他任務的影響,因為在生命周期完成的時候會自動clear所有的數據。Alibaba的一個庫解決了這個問題github:alibaba/transmittable-thread-local
如何使用
這個庫最簡單的方式是這樣使用的,通過簡單的修飾,使得提交的runable擁有了上一節所述的功能。具體的API文檔詳見github,這里不再贅述
TransmittableThreadLocal<String> parent = new TransmittableThreadLocal<String>();
parent.set("value-set-in-parent");
Runnable task = new Task("1");
// 額外的處理,生成修飾了的對象ttlRunnable
Runnable ttlRunnable = TtlRunnable.get(task);
executorService.submit(ttlRunnable);
// Task中可以讀取, 值是"value-set-in-parent"
String value = parent.get();
原理簡述
這個方法TtlRunnable.get(task)最終會調用構造方法,返回的是該類本身,也是一個Runable,這樣就完成了簡單的裝飾。最重要的是在run方法這個地方。
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值到本類當中
this.copiedRef = new AtomicReference<Map<TransmittableThreadLocal<?>, Object>>(TransmittableThreadLocal.copy());
this.runnable = runnable;//提交的runable,被修飾對象
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!");
}
//裝載到當前線程
Map<TransmittableThreadLocal<?>, Object> backup = TransmittableThreadLocal.backupAndSetToCopied(copied);
try {
runnable.run();//執行提交的task
} finally {
//clear
TransmittableThreadLocal.restoreBackup(backup);
}
}
}
在上面的使用線程池的例子當中,如果換成這種修飾的方式進行操作,B任務得到的肯定是父線程中ThreadLocal的值,解決了在線程池中InheritableThreadLocal不能解決的問題。
如何更新父線程ThreadLocal值?
如果線程之間出了要能夠得到父線程中的值,同時想更新值怎么辦呢?在前面我們有提到,當子線程copy父線程的ThreadLocalMap的時候是淺拷貝的,代表子線程Entry里面的value都是指向的同一個引用,我們只要修改這個引用的同時就能夠修改父線程當中的值了,比如這樣:
@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());
}
這樣父線程中的值就會得到更新了。能夠滿足父線程ThreadLocal值的實時更新,同時子線程也能共享父線程的值。不過場景倒是不是很常見的樣子。
參考文章
本文作者: catalinaLi
本文鏈接: http://catalinali.top/2018/helloThreadLocal/