《Java并發(fā)編程之美》讀書筆記
ThreadLocal
多線程在訪問同一個(gè)共享變量的時(shí)候容易出現(xiàn)并發(fā)的問題惊豺,特別是在多個(gè)線程對(duì)同一個(gè)共享變量進(jìn)行寫入的時(shí)候敬锐,一般都要對(duì)共享變量進(jìn)行適當(dāng)?shù)耐健?br>
同步的措施一般都是加鎖袜啃,這就需要使用者對(duì)鎖有一定的了解芳绩,這顯然增加了使用者的負(fù)擔(dān)蛾绎,那么有沒有一種方式可以做到昆箕,當(dāng)創(chuàng)建一個(gè)變量后,每個(gè)線程對(duì)其進(jìn)行訪問的時(shí)候訪問的是自己線程的變量呢->ThreadLocal
ThreadLocal是JDK包提供的租冠,它提供了線程本地變量鹏倘,也就是如果你創(chuàng)建了一個(gè)ThreadLocal,那么訪問這個(gè)變量的每個(gè)線程都會(huì)有這和個(gè)變量本地的一個(gè)副本顽爹。當(dāng)多個(gè)線程操作這個(gè)ThreadLocal變量時(shí)纤泵,實(shí)際上是在操作自己本地內(nèi)存里面的變量,從而避免了線程安全問題镜粤。創(chuàng)建了一個(gè)ThreadLocal變量后捏题,每個(gè)線程都會(huì)復(fù)制一個(gè)變量復(fù)制到自己的本地內(nèi)存
ThreadLocal使用示例
public class ThreadLocalTest {
static void print(String str){
//獲取到當(dāng)前線程本地內(nèi)存中的localVariable值
System.out.println(str+":"+localVariable.get());
//刪除當(dāng)前線程本地內(nèi)存中的localVariable值
localVariable.remove();
}
//創(chuàng)建ThreadLocal變量
static ThreadLocal<String> localVariable=new ThreadLocal<>();
public static void main(String[] args) {
Thread threadOne=new Thread(new Runnable() {
@Override
public void run() {
//設(shè)置當(dāng)前線程本地內(nèi)存中的localVariable值
localVariable.set("threadOne local variable");
print("threadOne");
System.out.println("threadOne remove after"+":"+localVariable.get());
}
});
Thread threadTwo=new Thread(new Runnable() {
@Override
public void run() {
localVariable.set("threadTwo local variable");
print("threadTwo");
System.out.println("threadTwo remove after"+":"+localVariable.get());
}
});
threadOne.start();
threadTwo.start();
}
}
本例子開啟了兩個(gè)線程,在每個(gè)線程內(nèi)部設(shè)置了本地變量的值肉渴,然后調(diào)用print函數(shù)打印當(dāng)前本地變量的值公荧,如果打印后調(diào)用了本地變量的remove方法之后,則會(huì)刪除本地內(nèi)存中的共享變量同规。
線程One run方法通過設(shè)置localvariable的值循狰,這其實(shí)是設(shè)置的是線程one本地內(nèi)存中的一個(gè)副本窟社,這個(gè)副本線程two是訪問不了的。
ThreadLocal實(shí)現(xiàn)原理
Thread類內(nèi)部會(huì)有一個(gè)threadLocals和inheritableThreadLocals,他們都是ThreadLocalMap類型的變量绪钥,而ThreadLocalMap是一個(gè)定制化的hashMap灿里,在默認(rèn)的情況下,每個(gè)線程中的兩個(gè)變量都為null昧识,只有當(dāng)前線程第一次調(diào)用ThreadLocal的set或者get方法之后才會(huì)創(chuàng)建他們钠四,其實(shí)每個(gè)線程的本地變量并不是存在ThreadLocal實(shí)例里面盗扒,而是存放在調(diào)用線程的threadLocals里面跪楞,也就是說ThreadLocal類型的本地變量存放在具體的線程的內(nèi)存空間中,ThreadLocal就是一個(gè)空殼侣灶,它通過set方法把value值放入線程的threadlocals里面存放起來甸祭,當(dāng)調(diào)用線程使用它的get方法時(shí),再從當(dāng)前線程的threadlocals變量里面將其拿出來褥影,如果調(diào)用線程一直不終止池户,那么這個(gè)本地變量會(huì)一直存放在調(diào)用線程的threadlocals里面,所以當(dāng)不需要使用本地變量的時(shí)候凡怎,可以通過調(diào)用ThreadLocal變量里面的remove方法校焦,從當(dāng)前線程的threadlocals里面刪除該本地變量
另外 Thread里面的threadlocals為什么設(shè)置為map結(jié)構(gòu)?很明顯是因?yàn)槎鄠€(gè)線程可以關(guān)聯(lián)多個(gè)ThreadLocal變量统倒。
簡單分析ThreadLocal的set寨典,get以及remove方法的是實(shí)現(xiàn)邏輯
- public void set(T value)
public void set(T value) {
//獲取到當(dāng)前線程
Thread t = Thread.currentThread();
//將當(dāng)前線程作為key去找對(duì)應(yīng)的線程變量,找到則設(shè)置
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
//如果是第一次調(diào)用就創(chuàng)建當(dāng)前線程對(duì)應(yīng)的HashMap
createMap(t, value);
}
}
代碼中首先獲取調(diào)用線程房匆,然后使用當(dāng)前線程作為參數(shù)調(diào)用getMap(t)方法
可以看到耸成,getMap(t)的作用是獲取線程自己的變量threadlocals,threadlocals變量被綁定到了線程的成員變量上。
如果getMap(t)返回值不為空浴鸿,則把value值設(shè)置到threadLocals中井氢,也就是把變量值放入到當(dāng)前線程的內(nèi)存變量threadLocals中。threadLocals是一個(gè)HashMap結(jié)構(gòu)岳链,其中的key就是當(dāng)前ThreadLocal的實(shí)例對(duì)象的引用花竞,value是通過set方法傳遞的值
如果getMap(t)返回空值則說明是第一次調(diào)用set方法,這時(shí)用 createMap(t, value)創(chuàng)建當(dāng)前線程的threadLocals變量掸哑。
createMap創(chuàng)建當(dāng)前線程的threadLocals變量约急。
- T get()
public T get() {
//獲取當(dāng)前線程
Thread t = Thread.currentThread();
//獲取當(dāng)前線程的threadLocals變量 是一個(gè)ThreadLocalMap結(jié)構(gòu)
ThreadLocalMap map = getMap(t);
//如果threadLocals不為空,則返回對(duì)應(yīng)本地變量的值
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//threadLocals為空的話就初始化當(dāng)前線程的threadLocals變量
return setInitialValue();
}
上訴代碼首先獲取當(dāng)前線程的實(shí)例举户,在獲取當(dāng)前線程的threadLocals變量烤宙,如果不為null則直接返回當(dāng)前線程綁定的本地變量,否則執(zhí)行代碼初始化俭嘁。
private T setInitialValue() {
//初始化為null
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//如果當(dāng)前線程的threadLocals變量不為空
if (map != null) {
//value為null
map.set(this, value);
} else {
//如果當(dāng)前線程的threadLocals變量為空
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
如果當(dāng)前線程的threadLocals變量不為空躺枕,則設(shè)置為當(dāng)前線程的本地變量值為null,否則調(diào)用createMap(t, value)方法創(chuàng)建當(dāng)前線程的threadLocals變量。
-
void remove()
如果當(dāng)前線程的threadLocals變量不為空拐云,則刪除當(dāng)前線程中指定ThreadLocal實(shí)例即this的本地變量罢猪。
總結(jié)
在每個(gè)線程內(nèi)部都有一個(gè)名為threadLocals的成員變量,這個(gè)變量的類型是HashMap叉瘩,其中key就為我們定義的ThreadLocal類型的變量的this引用膳帕,value則為我們用set方法設(shè)置的值,每個(gè)線程的本地變量存放在線程自己的內(nèi)存變量threadLocals里面薇缅,如果當(dāng)前線程一直不消亡危彩,那么這些本地變量就會(huì)一直存在,所以可能會(huì)造成內(nèi)存溢出泳桦,因此使用完畢后記得調(diào)用ThreadLocal的remove方法刪除對(duì)應(yīng)線程的threadLocals中的本地變量汤徽。
注:在JUC包里面的ThreadLocalRandom,就是借鑒這中思想實(shí)現(xiàn)的灸撰。
ThreadLocal不支持繼承性
public class ThreadLocalDemo {
//創(chuàng)建線程變量
private static ThreadLocal<String> threadLocal=new ThreadLocal<>();
public static void main(String[] args) {
threadLocal.set("helloworld");
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread:"+threadLocal.get());
}
});
thread.start();
System.out.println("main:"+threadLocal.get());
}
}
也就是說谒府,同一個(gè)ThreadLocal變量在父線程中設(shè)置值后,在子線程中是獲取不到的浮毯,如之前所說完疫,這是很正常的現(xiàn)象,因?yàn)樽泳€程thread里面調(diào)用get方法時(shí)當(dāng)前線程為thread線程债蓝,而這里調(diào)用set方法設(shè)置線程變量的是main線程壳鹤,兩者是不同的線程所以子線程訪問時(shí)為null;
Inheritable ThreadLocal類
為了解決ThreadLocal類不支持繼承性的這個(gè)問題惦蚊,InheritableThreadLocal應(yīng)運(yùn)而生器虾,它繼承自ThreadLocal,提供了一個(gè)屬性蹦锋,就是讓子線程可以訪問可以訪問在父線程中設(shè)置的本地變量兆沙,
InheritableThreadLocal源代碼:
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
由以上代碼可知,InheritableThreadLocal繼承ThreadLocal類莉掂,并且重寫了三個(gè)方法葛圃。重寫了createMap方法,所以現(xiàn)在第一次調(diào)用set方法的時(shí)候憎妙,創(chuàng)建的是當(dāng)前線程的t.inheritableThreadLocals變量的實(shí)例而不再是threadLocals實(shí)例再沧。當(dāng)調(diào)用get方法獲取當(dāng)前線程內(nèi)部的map變量時(shí)杨拐,獲取的是t.inheritableThreadLocals而不再是threadLocals裂七。
綜上鹃操,在InheritableThreadLocal世界里,變量由inheritableThreadLocals代替了threadLocals
觀察如何讓子線程可以訪問父線程的本地變量抚垃。
public Thread(Runnable target) {
this(null, target, "Thread-" + nextThreadNum(), 0);
}
private Thread(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
//獲取當(dāng)前線程
Thread parent = currentThread();
//如果父線程的inheritThreadLocals不為null
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
//設(shè)置子線程中的inheritThreadLocals變量
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
this.stackSize = stackSize;
this.tid = nextThreadID();
}
如上的代碼在創(chuàng)建的過程中喷楣,在構(gòu)造函數(shù)中會(huì)調(diào)用私有的構(gòu)造方法趟大,先獲取當(dāng)前的線程,這里是main函數(shù)所在的線程铣焊,也就是main線程逊朽,然后再判斷main函數(shù)所在的線程里面inheritableThreadLocals是否為空,然后就會(huì)執(zhí)行createInheritedMap方法
可以看到createInheritedMap的內(nèi)部使用的是父線程的inheritableThreadLocals變量作為構(gòu)造函數(shù)創(chuàng)建了一個(gè)新的ThreadLocalMap變量曲伊。
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (Entry e : parentTable) {
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
在該構(gòu)造函數(shù)的內(nèi)部將父線程的inheritableThreadLocals成員變量復(fù)制到新的ThreadLocalMap變量當(dāng)中
總結(jié)叽讳,InheritableThreadLocal通過重寫ThreadLocal類的代碼讓本地變量保存到了具體的inheritableThreadLocals里面,那么線程在通過InheritableThreadLocal實(shí)例的set或者get方法設(shè)置變量時(shí)坟募,就會(huì)創(chuàng)建當(dāng)前線程的inheritableThreadLocals變量岛蚤。當(dāng)父線程創(chuàng)建子線程時(shí),構(gòu)造函數(shù)會(huì)把父線程中的inheritableThreadLocals變量里面的本地變量復(fù)制一份保存到子線程的inheritableThreadLocals里面
把之前的代碼改為:
private static ThreadLocal<String> threadLocal=new InheritableThreadLocal<>();
可見婿屹,現(xiàn)在可以從子線程正常獲取到線程變量的值了灭美。
在什么情況下需要子線程可以獲取到父線程的thredLocal變量呢推溃?
比如子線程需要使用存放在threadLocal變量中的用戶信息昂利,再比如說一些中間件需要把統(tǒng)一的id追蹤的整個(gè)調(diào)用鏈路記錄下來。其實(shí)子線程使用父線程中的threadLocal方法有很多種铁坎,比如創(chuàng)建線程時(shí)候蜂奸,使用父線程中的變量,并將其復(fù)制到子線程中硬萍,或者在父線程中構(gòu)造一個(gè)map作為參數(shù)傳遞給子線程扩所。但是這些方法都改變了我們的使用習(xí)慣,所以在這些情況下InheritableThreadLocal就顯得比較有用朴乖。
參考資料:
《Java并發(fā)編程之美》