- Java并發(fā)編程之美
- 翟陸續(xù) 薛賓田
- 3498字
- 2019-07-25 11:54:02
1.11 ThreadLocal
多線程訪問同一個共享變量時特別容易出現(xiàn)并發(fā)問題,特別是在多個線程需要對一個共享變量進行寫入時。為了保證線程安全,一般使用者在訪問共享變量時需要進行適當(dāng)?shù)耐剑鐖D1-3所示。
同步的措施一般是加鎖,這就需要使用者對鎖有一定的了解,這顯然加重了使用者的負擔(dān)。那么有沒有一種方式可以做到,當(dāng)創(chuàng)建一個變量后,每個線程對其進行訪問的時候訪問的是自己線程的變量呢?其實ThreadLocal就可以做這件事情,雖然ThreadLocal并不是為了解決這個問題而出現(xiàn)的。

圖1-3
ThreadLocal是JDK包提供的,它提供了線程本地變量,也就是如果你創(chuàng)建了一個ThreadLocal變量,那么訪問這個變量的每個線程都會有這個變量的一個本地副本。當(dāng)多個線程操作這個變量時,實際操作的是自己本地內(nèi)存里面的變量,從而避免了線程安全問題。創(chuàng)建一個ThreadLocal變量后,每個線程都會復(fù)制一個變量到自己的本地內(nèi)存,如圖1-4所示。

圖1-4
1.11.1 ThreadLocal使用示例
本節(jié)介紹如何使用ThreadLocal。本例開啟了兩個線程,在每個線程內(nèi)部都設(shè)置了本地變量的值,然后調(diào)用print函數(shù)打印當(dāng)前本地變量的值。如果打印后調(diào)用了本地變量的remove方法,則會刪除本地內(nèi)存中的該變量,代碼如下。
public class ThreadLocalTest { //(1)print函數(shù) static void print(String str){ //1.1 打印當(dāng)前線程本地內(nèi)存中l(wèi)ocalVariable變量的值 System.out.println(str + ":" +localVariable.get()); //1.2 清除當(dāng)前線程本地內(nèi)存中的localVariable變量 //localVariable.remove(); } //(2) 創(chuàng)建ThreadLocal變量 static ThreadLocal<String> localVariable = new ThreadLocal<>(); public static void main(String[] args) { //(3) 創(chuàng)建線程one Thread threadOne = new Thread(new Runnable() { public void run() { //3.1 設(shè)置線程One中本地變量localVariable的值 localVariable.set("threadOne local variable"); //3.2 調(diào)用打印函數(shù) print("threadOne"); //3.3 打印本地變量值 System.out.println("threadOne remove after" + ":" +localVariable.get()); } });
//(4) 創(chuàng)建線程two Thread threadTwo = new Thread(new Runnable() { public void run() { //4.1 設(shè)置線程Two中本地變量localVariable的值 localVariable.set("threadTwo local variable"); //4.2 調(diào)用打印函數(shù) print("threadTwo"); //4.3 打印本地變量值 System.out.println("threadTwo remove after" + ":" +localVariable.get()); } }); //(5)啟動線程 threadOne.start(); threadTwo.start(); }
運行結(jié)果如下。
threadOne:threadOne local variable threadTwo:threadTwo local variable threadOne remove after:threadOne local variable threadTwo remove after:threadTwo local variable
代碼(2)創(chuàng)建了一個ThreadLocal變量。
代碼(3)和(4)分別創(chuàng)建了線程One和Two。
代碼(5)啟動了兩個線程。
線程One中的代碼3.1通過set方法設(shè)置了localVariable的值,這其實設(shè)置的是線程One本地內(nèi)存中的一個副本,這個副本線程Two是訪問不了的。然后代碼3.2調(diào)用了print函數(shù),代碼1.1通過get函數(shù)獲取了當(dāng)前線程(線程One)本地內(nèi)存中l(wèi)ocalVariable的值。
線程Two的執(zhí)行類似于線程One。
打開代碼1.2的注釋后,再次運行,運行結(jié)果如下。
threadOne:threadOne local variable threadOne remove after:null threadTwo:threadTwo local variable threadTwo remove after:null
1.11.2 ThreadLocal的實現(xiàn)原理
首先看一下ThreadLocal相關(guān)類的類圖結(jié)構(gòu),如圖1-5所示。

圖1-5
由該圖可知,Thread類中有一個threadLocals和一個inheritableThreadLocals,它們都是ThreadLocalMap類型的變量,而ThreadLocalMap是一個定制化的Hashmap。在默認情況下,每個線程中的這兩個變量都為null,只有當(dāng)前線程第一次調(diào)用ThreadLocal的set或者get方法時才會創(chuàng)建它們。其實每個線程的本地變量不是存放在ThreadLocal實例里面,而是存放在調(diào)用線程的threadLocals變量里面。也就是說,ThreadLocal類型的本地變量存放在具體的線程內(nèi)存空間中。ThreadLocal就是一個工具殼,它通過set方法把value值放入調(diào)用線程的threadLocals里面并存放起來,當(dāng)調(diào)用線程調(diào)用它的get方法時,再從當(dāng)前線程的threadLocals變量里面將其拿出來使用。如果調(diào)用線程一直不終止,那么這個本地變量會一直存放在調(diào)用線程的threadLocals變量里面,所以當(dāng)不需要使用本地變量時可以通過調(diào)用ThreadLocal變量的remove方法,從當(dāng)前線程的threadLocals里面刪除該本地變量。另外,Thread里面的threadLocals為何被設(shè)計為map結(jié)構(gòu)?很明顯是因為每個線程可以關(guān)聯(lián)多個ThreadLocal變量。
下面簡單分析ThreadLocal的set、get及remove方法的實現(xiàn)邏輯。
1.void set(T value)
public void set(T value) {
//(1)獲取當(dāng)前線程 Thread t = Thread.currentThread(); //(2)將當(dāng)前線程作為key,去查找對應(yīng)的線程變量,找到則設(shè)置 ThreadLocalMap map = getMap(t); if (map ! = null) map.set(this, value); else //(3)第一次調(diào)用就創(chuàng)建當(dāng)前線程對應(yīng)的HashMap createMap(t, value); }
代碼(1)首先獲取調(diào)用線程,然后使用當(dāng)前線程作為參數(shù)調(diào)用getMap(t)方法,getMap(Thread t)的代碼如下。
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
可以看到,getMap(t)的作用是獲取線程自己的變量threadLocals, threadlocal變量被綁定到了線程的成員變量上。
如果getMap(t)的返回值不為空,則把value值設(shè)置到threadLocals中,也就是把當(dāng)前變量值放入當(dāng)前線程的內(nèi)存變量threadLocals中。threadLocals是一個HashMap結(jié)構(gòu),其中key就是當(dāng)前ThreadLocal的實例對象引用,value是通過set方法傳遞的值。
如果getMap(t)返回空值則說明是第一次調(diào)用set方法,這時創(chuàng)建當(dāng)前線程的threadLocals變量。下面來看createMap(t, value)做什么。
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
它創(chuàng)建當(dāng)前線程的threadLocals變量。
2.T get()
public T get() { //(4) 獲取當(dāng)前線程 Thread t = Thread.currentThread(); //(5)獲取當(dāng)前線程的threadLocals變量 ThreadLocalMap map = getMap(t); //(6)如果threadLocals不為null,則返回對應(yīng)本地變量的值 if (map ! = null) {
ThreadLocalMap.Entry e = map.getEntry(this); if (e ! = null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } //(7)threadLocals為空則初始化當(dāng)前線程的threadLocals成員變量 return setInitialValue(); }
代碼(4)首先獲取當(dāng)前線程實例,如果當(dāng)前線程的threadLocals變量不為null,則直接返回當(dāng)前線程綁定的本地變量,否則執(zhí)行代碼(7)進行初始化。setInitialValue()的代碼如下。
private T setInitialValue() { //(8)初始化為null T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); //(9)如果當(dāng)前線程的threadLocals變量不為空 if (map ! = null) map.set(this, value); else //(10)如果當(dāng)前線程的threadLocals變量為空 createMap(t, value); return value; } protected T initialValue() { return null; }
如果當(dāng)前線程的threadLocals變量不為空,則設(shè)置當(dāng)前線程的本地變量值為null,否則調(diào)用createMap方法創(chuàng)建當(dāng)前線程的createMap變量。
3.void remove()
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m ! = null) m.remove(this); }
如以上代碼所示,如果當(dāng)前線程的threadLocals變量不為空,則刪除當(dāng)前線程中指定ThreadLocal實例的本地變量。
總結(jié):如圖1-6所示,在每個線程內(nèi)部都有一個名為threadLocals的成員變量,該變量的類型為HashMap,其中key為我們定義的ThreadLocal變量的this引用,value則為我們使用set方法設(shè)置的值。每個線程的本地變量存放在線程自己的內(nèi)存變量threadLocals中,如果當(dāng)前線程一直不消亡,那么這些本地變量會一直存在,所以可能會造成內(nèi)存溢出,因此使用完畢后要記得調(diào)用ThreadLocal的remove方法刪除對應(yīng)線程的threadLocals中的本地變量。在高級篇要講解的JUC包里面的ThreadLocalRandom,就是借鑒ThreadLocal的思想實現(xiàn)的,后面會具體講解。

圖1-6
1.11.3 ThreadLocal不支持繼承性
首先看一個例子。
public class TestThreadLocal { //(1)創(chuàng)建線程變量 public static ThreadLocal<String> threadLocal = new ThreadLocal<String>(); public static void main(String[] args) {
//(2) 設(shè)置線程變量 threadLocal.set("hello world"); //(3) 啟動子線程 Thread thread = new Thread(new Runnable() { public void run() { //(4) 子線程輸出線程變量的值 System.out.println("thread:" + threadLocal.get()); } }); thread.start(); //(5) 主線程輸出線程變量的值 System.out.println("main:" + threadLocal.get()); } }
輸出結(jié)果如下。
main:hello world thread:null
也就是說,同一個ThreadLocal變量在父線程中被設(shè)置值后,在子線程中是獲取不到的。根據(jù)上節(jié)的介紹,這應(yīng)該是正常現(xiàn)象,因為在子線程thread里面調(diào)用get方法時當(dāng)前線程為thread線程,而這里調(diào)用set方法設(shè)置線程變量的是main線程,兩者是不同的線程,自然子線程訪問時返回null。那么有沒有辦法讓子線程能訪問到父線程中的值?答案是有。
1.11.4 InheritableThreadLocal類
為了解決上節(jié)提出的問題,InheritableThreadLocal應(yīng)運而生。InheritableThreadLocal繼承自ThreadLocal,其提供了一個特性,就是讓子線程可以訪問在父線程中設(shè)置的本地變量。下面看一下InheritableThreadLocal的代碼。
public class InheritableThreadLocal<T> extends ThreadLocal<T> { //(1) protected T childValue(T parentValue) { return parentValue; } //(2)
ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } //(3) void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); } }
由如上代碼可知,InheritableThreadLocal繼承了ThreadLocal,并重寫了三個方法。由代碼(3)可知,InheritableThreadLocal重寫了createMap方法,那么現(xiàn)在當(dāng)?shù)谝淮握{(diào)用set方法時,創(chuàng)建的是當(dāng)前線程的inheritableThreadLocals變量的實例而不再是threadLocals。由代碼(2)可知,當(dāng)調(diào)用get方法獲取當(dāng)前線程內(nèi)部的map變量時,獲取的是inheritableThreadLocals而不再是threadLocals。
綜上可知,在InheritableThreadLocal的世界里,變量inheritableThreadLocals替代了threadLocals。
下面我們看一下重寫的代碼(1)何時執(zhí)行,以及如何讓子線程可以訪問父線程的本地變量。這要從創(chuàng)建Thread的代碼說起,打開Thread類的默認構(gòu)造函數(shù),代碼如下。
public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); } private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc) { ... //(4)獲取當(dāng)前線程 Thread parent = currentThread(); ... //(5)如果父線程的inheritableThreadLocals變量不為null if (parent.inheritableThreadLocals ! = null) //(6)設(shè)置子線程中的inheritableThreadLocals變量 this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); this.stackSize = stackSize; tid = nextThreadID(); }
如上代碼在創(chuàng)建線程時,在構(gòu)造函數(shù)里面會調(diào)用init方法。代碼(4)獲取了當(dāng)前線程(這里是指main函數(shù)所在的線程,也就是父線程),然后代碼(5)判斷main函數(shù)所在線程里面的inheritableThreadLocals屬性是否為null,前面我們講了InheritableThreadLocal類的get和set方法操作的是inheritableThreadLocals,所以這里的inheritableThreadLocal變量不為null,因此會執(zhí)行代碼(6)。下面看一下createInheritedMap的代碼。
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { return new ThreadLocalMap(parentMap); }
可以看到,在createInheritedMap內(nèi)部使用父線程的inheritableThreadLocals變量作為構(gòu)造函數(shù)創(chuàng)建了一個新的ThreadLocalMap變量,然后賦值給了子線程的inheritableThreadLocals變量。下面我們看看在ThreadLocalMap的構(gòu)造函數(shù)內(nèi)部都做了什么事情。
private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e ! = null) { @SuppressWarnings("unchecked") ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key ! = null) { //(7)調(diào)用重寫的方法 Object value = key.childValue(e.value); //返回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對象中,其中代碼(7)調(diào)用了InheritableThreadLocal類重寫的代碼(1)。
總結(jié):InheritableThreadLocal類通過重寫代碼(2)和(3)讓本地變量保存到了具體線程的inheritableThreadLocals變量里面,那么線程在通過InheritableThreadLocal類實例的set或者get方法設(shè)置變量時,就會創(chuàng)建當(dāng)前線程的inheritableThreadLocals變量。當(dāng)父線程創(chuàng)建子線程時,構(gòu)造函數(shù)會把父線程中inheritableThreadLocals變量里面的本地變量復(fù)制一份保存到子線程的inheritableThreadLocals變量里面。
把1.11.3節(jié)中的代碼(1)修改為
//(1) 創(chuàng)建線程變量 public static ThreadLocal<String> threadLocal = new InheritableThreadLocal<Stri ng>();
運行結(jié)果如下。
thread:hello world main:hello world
可見,現(xiàn)在可以從子線程正常獲取到線程變量的值了。
那么在什么情況下需要子線程可以獲取父線程的threadlocal變量呢?情況還是蠻多的,比如子線程需要使用存放在threadlocal變量中的用戶登錄信息,再比如一些中間件需要把統(tǒng)一的id追蹤的整個調(diào)用鏈路記錄下來。其實子線程使用父線程中的threadlocal方法有多種方式,比如創(chuàng)建線程時傳入父線程中的變量,并將其復(fù)制到子線程中,或者在父線程中構(gòu)造一個map作為參數(shù)傳遞給子線程,但是這些都改變了我們的使用習(xí)慣,所以在這些情況下InheritableThreadLocal就顯得比較有用。
- HTML5+CSS3+JavaScript從入門到精通:上冊(微課精編版·第2版)
- JavaScript高效圖形編程
- Visual Basic編程:從基礎(chǔ)到實踐(第2版)
- Xcode 7 Essentials(Second Edition)
- Wireshark Network Security
- C語言課程設(shè)計
- Python Data Analysis Cookbook
- jQuery Mobile移動應(yīng)用開發(fā)實戰(zhàn)(第3版)
- Lighttpd源碼分析
- 計算機應(yīng)用基礎(chǔ)教程(Windows 7+Office 2010)
- Unity 2017 Game AI Programming(Third Edition)
- Hands-On Robotics Programming with C++
- Head First Kotlin程序設(shè)計
- INSTANT EaselJS Starter
- Elasticsearch實戰(zhàn)(第2版)