快速了解Java中ThreadLocal類
最近看Android FrameWork層代碼,看到了ThreadLocal這個(gè)類,有點(diǎn)兒陌生,就翻了各種相關(guān)博客一一拜讀;自己隨后又研究了一遍源碼,發(fā)現(xiàn)自己的理解較之前閱讀的博文有不同之處,所以決定自己寫篇文章說(shuō)說(shuō)自己的理解,希望可以起到以下作用:
- 可以疏通研究結(jié)果,加深自己的理解;
- 可以起到拋磚引玉的作用,幫助感興趣的同學(xué)疏通思路;
- 分享學(xué)習(xí)經(jīng)歷,同大家一起交流和學(xué)習(xí)。
一、 ThreadLocal 是什么
ThreadLocal 是Java類庫(kù)的基礎(chǔ)類,在包java.lang下面;
官方的解釋是這樣的:
Implements a thread-local storage, that is, a variable for which each thread has its own value. All threads share the same ThreadLocal object, but each sees a different value when accessing it, and changes made by one thread do not affect the other threads. The implementation supports null values.
大致意思是:
可以實(shí)現(xiàn)線程的本地存儲(chǔ)機(jī)制,ThreadLocal變量是一個(gè)不同線程可以擁有不同值的變量。所有的線程可以共享同一個(gè)ThreadLocal對(duì)象,但是不同線程訪問(wèn)的時(shí)候可以取得不同的值,而且任意一個(gè)線程對(duì)它的改變不會(huì)影響其他線程。類實(shí)現(xiàn)是支持null值的(可以在set和get方法傳遞和訪問(wèn)null值)。
概括來(lái)講有三個(gè)特性:
- 不同線程訪問(wèn)時(shí)取得不同的值
- 任意線程對(duì)它的改變不影響其他線程
- 支持null
下面分別對(duì)這些特性進(jìn)行實(shí)例驗(yàn)證,首先定義一個(gè)Test類,在此類中我們鑒證上邊所提到的三個(gè)特性。類定義如下:
Test.java
public class Test{ //定義ThreadLocal private static ThreadLocal name; public static void main(String[] args) throws Exception{ name = new ThreadLocal(); //Define Thread A Thread a = new Thread(){ public void run(){ System.out.println("Before invoke set,value is:"+name.get()); name.set(“Thread A”); System.out.println("After invoke set, value is:"+name.get()); } } ; //Define Thread B Thread b = new Thread(){ public void run(){ System.out.println("Before invoke set,value is :"+name.get()); name.set(“Thread B”); System.out.println("After invoke set,value is :"+name.get()); } } ; // Not invoke set, print the value is null System.out.println(name.get()); // Invoke set to fill a value name.set(“Thread Main”); // Start thread A a.start(); a.join(); // Print the value after changed the value by thread A System.out.println(name.get()); // Start thread B b.start(); b.join(); // Print the value after changed the value by thread B System.out.println(name.get()) } }
代碼分析:
從定義中我們可以看到只聲明了一個(gè)ThreadLocal對(duì)象,其他三個(gè)線程(主線程、Thread A和Thread B)共享同一個(gè)對(duì)象;然后,在不同的線程中修改對(duì)象的值和在不同的線程中訪問(wèn)對(duì)象的值,并在控制臺(tái)輸出查看結(jié)果。
看結(jié)果:
從控制臺(tái)輸出結(jié)果可以看到里邊有三個(gè)null的輸出,這個(gè)是因?yàn)樵谳敵銮皼](méi)有對(duì)對(duì)象進(jìn)行賦值,驗(yàn)證了支持null的特點(diǎn);再者,還可以發(fā)現(xiàn)在每個(gè)線程我都對(duì)對(duì)象的值做了修改,但是在其他線程訪問(wèn)對(duì)象時(shí)并不是修改后的值,而是訪問(wèn)線程本地的值;這樣也驗(yàn)證了其他兩個(gè)特點(diǎn)。
二、 ThreadLocal的作用
大家都知道它的使用場(chǎng)景大都是多線程編程,至于具體的作用,這個(gè)怎么說(shuō)那?我覺(jué)得這個(gè)只能用一個(gè)泛的說(shuō)法來(lái)定義,因?yàn)橐粋€(gè)東西的功能屬性定義了以后會(huì)限制大家的思路,就好比說(shuō)菜刀是用來(lái)切菜的,好多人就不會(huì)用它切西瓜了。
這里,說(shuō)下我對(duì)它的作用的認(rèn)識(shí),僅供參考,希望能有所幫助。這樣來(lái)描述吧,當(dāng)一個(gè)多線程的程序需要對(duì)多數(shù)線程的部分任務(wù)(就是run方法里的部分代碼)進(jìn)行封裝時(shí),在封裝體里就可以用ThreadLocal來(lái)包裝與線程相關(guān)的成員變量,從而保證線程訪問(wèn)的獨(dú)占性,而且所有線程可以共享一個(gè)封裝體對(duì)象;可以參考下Android里的Looper。不會(huì)用代碼描述問(wèn)題的程序員不是好程序員;
看代碼:統(tǒng)計(jì)線程某段代碼耗時(shí)的工具(為說(shuō)明問(wèn)題自造)
StatisticCostTime.java
// Class that statistic the cost time public class StatisticCostTime{ // record the startTime // private ThreadLocal startTime = new ThreadLocal(); private long startTime; // private ThreadLocal costTime = new ThreadLocal(); private long costTime; private StatisticCostTime(){ } //Singleton public static final StatisticCostTime shareInstance(){ return InstanceFactory.instance; } private static class InstanceFactory{ private static final StatisticCostTime instance = new StatisticCostTime(); } // start public void start(){ // startTime.set(System. nanoTime ()); startTime = System.nanoTime(); } // end public void end(){ // costTime.set(System. nanoTime () - startTime.get()); costTime = System.nanoTime() - startTime; } public long getStartTime(){ return startTime; // return startTime.get(); } public long getCostTime(){ // return costTime.get(); return costTime; }
好了,工具設(shè)計(jì)完工了,現(xiàn)在我們用它來(lái)統(tǒng)計(jì)一下線程耗時(shí)試試唄:
Main.java
public class Main{ public static void main(String[] args) throws Exception{ // Define the thread a Thread a = new Thread(){ public void run(){ try{ // start record time StatisticCostTime.shareInstance().start(); sleep(200); // print the start time of A System.out.println("A-startTime:"+StatisticCostTime.shareInstance().getStartTime()); // end the record StatisticCostTime.shareInstance().end(); // print the costTime of A System.out.println("A:"+StatisticCostTime.shareInstance().getCostTime()); } catch(Exception e){ } } } ; // start a a.start(); // Define thread b Thread b = new Thread(){ public void run(){ try{ // record the start time of B1 StatisticCostTime.shareInstance().start(); sleep(100); // print the start time to console System.out.println("B1-startTime:"+StatisticCostTime.shareInstance().getStartTime()); // end record start time of B1 StatisticCostTime.shareInstance().end(); // print the cost time of B1 System.out.println("B1:"+StatisticCostTime.shareInstance().getCostTime()); // start record time of B2 StatisticCostTime.shareInstance().start(); sleep(100); // print start time of B2 System.out.println("B2-startTime:"+StatisticCostTime.shareInstance().getStartTime()); // end record time of B2 StatisticCostTime.shareInstance().end(); // print cost time of B2 System.out.println("B2:"+StatisticCostTime.shareInstance().getCostTime()); } catch(Exception e){ } } } ; b.start(); } }
運(yùn)行代碼后輸出結(jié)果是這樣的
注意:輸出結(jié)果精確度為納秒級(jí)
看結(jié)果是不是和我們預(yù)想的不一樣,發(fā)現(xiàn)A的結(jié)果應(yīng)該約等于B1+B2才對(duì)呀,怎么變成和B2一樣了那?答案就是我們?cè)诙xstartTime和costTime變量時(shí),本意是不應(yīng)共享的,應(yīng)是線程獨(dú)占的才對(duì)。而這里變量隨單例共享了,所以當(dāng)計(jì)算A的值時(shí),其實(shí)startTime已經(jīng)被B2修改了,所以就輸出了和B2一樣的結(jié)果。
現(xiàn)在我們把StatisticCostTime中注釋掉的部分打開(kāi),換成ThreadLocal的聲明方式試下。
看結(jié)果:
呀!這下達(dá)到預(yù)期效果了,這時(shí)候有同學(xué)會(huì)說(shuō)這不是可以線程并發(fā)訪問(wèn)了嗎,是不是只要我用了ThreadLocal就可以保證線程安全了?答案是no!首先先弄明白為什么會(huì)有線程安全問(wèn)題,無(wú)非兩種情況:
1、不該共享的資源,你在線程間共享了;
2、線程間共享的資源,你沒(méi)有保證有序訪問(wèn);
前者可以用“空間換時(shí)間”的方式解決,用ThreadLocal(也可以直接聲明線程局部變量),后者用“時(shí)間換空間”的方式解決,顯然這個(gè)就不是ThreadLocal力所能及的了。
三、 ThreadLocal 原理
實(shí)現(xiàn)原理其實(shí)很簡(jiǎn)單,每次對(duì)ThreadLocal 對(duì)象的讀寫操作其實(shí)是對(duì)線程的Values對(duì)象的讀寫操作;這里澄清一下,沒(méi)有什么變量副本的創(chuàng)建,因?yàn)榫蜎](méi)有用變量分配的內(nèi)存空間來(lái)存T對(duì)象的,而是用它所在線程的Values來(lái)存T對(duì)象的;我們?cè)诰€程中每次調(diào)用ThreadLocal的set方法時(shí),實(shí)際上是將object寫入線程對(duì)應(yīng)Values對(duì)象的過(guò)程;調(diào)用ThreadLocal的get方法時(shí),實(shí)際上是從線程對(duì)應(yīng)Values對(duì)象取object的過(guò)程。
看源碼:
ThreadLocal 的成員變量set
/** * Sets the value of this variable for the current thread. If set to * {@code null}, the value will be set to null and the underlying entry will * still be present. * * @param value the new value of the variable for the caller thread. */ public void set(T value) { Thread currentThread = Thread.currentThread(); Values values = values(currentThread); if (values == null) { values = initializeValues(currentThread); } values.put(this, value); }
TreadLocal 的成員方法get
/** * Returns the value of this variable for the current thread. If an entry * doesn't yet exist for this variable on this thread, this method will * create an entry, populating the value with the result of * {@link #initialValue()}. * * @return the current value of the variable for the calling thread. */ @SuppressWarnings("unchecked") public T get() { // Optimized for the fast path. Thread currentThread = Thread.currentThread(); Values values = values(currentThread); if (values != null) { Object[] table = values.table; int index = hash & values.mask; if (this.reference == table[index]) { return (T) table[index + 1]; } } else { values = initializeValues(currentThread); } return (T) values.getAfterMiss(this); }
ThreadLocal的成員方法initializeValues
/** * Creates Values instance for this thread and variable type. */ Values initializeValues(Thread current) { return current.localValues = new Values(); }
ThreadLocal 的成員方法values
/** * Gets Values instance for this thread and variable type. */ Values values(Thread current) { return current.localValues; }
那這個(gè)Values又是怎樣讀寫Object那?
Values是作為ThreadLocal的內(nèi)部類存在的;這個(gè)Values里包括了一個(gè)重要數(shù)組Object[],這個(gè)數(shù)據(jù)就是解答問(wèn)題的關(guān)鍵部分,它是用來(lái)存儲(chǔ)線程本地各種類型TreadLocal變量用的;那么問(wèn)題來(lái)了,具體取某個(gè)類型的變量時(shí)是怎么保證不取到其他類型的值那?按一般的做法會(huì)用一個(gè)Map根據(jù)key-value映射一下的;對(duì)的,思路就是這個(gè)思路,但是這里并沒(méi)有用Map來(lái)實(shí)現(xiàn),是用一個(gè)Object[]實(shí)現(xiàn)的Map機(jī)制;但是,若要用Map理解的話,也是不可以的,因?yàn)闄C(jī)制是相同的;key其實(shí)上對(duì)應(yīng)ThreadLocal的弱引用,value就對(duì)應(yīng)我們傳進(jìn)去的Object。
解釋下是怎么用Object[]實(shí)現(xiàn)Map機(jī)制的(參考圖1);它是用數(shù)組下標(biāo)的奇偶來(lái)區(qū)分key和value的,就是下表是偶數(shù)的位置存儲(chǔ)key,奇數(shù)存儲(chǔ)value,就是這樣搞得;感興趣的同學(xué)如果想知道算法實(shí)現(xiàn)的話,可以深入研究一下,這里我不在詳述了。
結(jié)合前面第一個(gè)實(shí)例分析下存儲(chǔ)情況:
當(dāng)程序執(zhí)行時(shí)存在A,B和main三個(gè)線程,分別在線程中調(diào)用name.set()時(shí)同時(shí)針對(duì)三個(gè)線程實(shí)例在堆區(qū)分配了三塊相同的內(nèi)存空間來(lái)存儲(chǔ)Values對(duì)象,以name引用作為key,具體的object作為值存進(jìn)三個(gè)不同的Object[](參看下圖):
四、 總結(jié)
ThreadLocal 不能完全解決多線程編程時(shí)的并發(fā)問(wèn)題,這種問(wèn)題還要根據(jù)不同的情況選擇不同的解決方案,“空間換時(shí)間”還是“時(shí)間換空間”。
ThreadLocal最大的作用就是把線程共享變量轉(zhuǎn)換成線程本地變量,實(shí)現(xiàn)線程之間的隔離。
以上就是本文關(guān)于快速了解Java中ThreadLocal的全部?jī)?nèi)容,希望對(duì)大家有所幫助。如有不足之處,歡迎留言指出。感謝朋友們對(duì)本站的支持。
- Java 中ThreadLocal類詳解
- 深入解析Java中ThreadLocal線程類的作用和用法
- Java多線程編程中ThreadLocal類的用法及深入
- Java ThreadLocal類應(yīng)用實(shí)戰(zhàn)案例分析
- JAVA開(kāi)發(fā)常用類庫(kù)UUID、Optional、ThreadLocal、TimerTask、Base64使用方法與實(shí)例詳解
- 詳解Java中ThreadLocal類型及簡(jiǎn)單用法
- Java 超詳細(xì)講解ThreadLocal類的使用
- Java并發(fā)編程ThreadLocalRandom類詳解
- Java ThreadLocal類使用詳解
相關(guān)文章
Spring Cloud Hystrix 服務(wù)容錯(cuò)保護(hù)的原理實(shí)現(xiàn)
這篇文章主要介紹了Spring Cloud Hystrix 服務(wù)容錯(cuò)保護(hù)的原理實(shí)現(xiàn),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-05-05使用kotlin編寫spring cloud微服務(wù)的過(guò)程
這篇文章主要介紹了使用kotlin編寫spring cloud微服務(wù)的相關(guān)知識(shí),本文給大家提到配置文件的操作代碼,給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-09-09Java 是如何利用接口避免函數(shù)回調(diào)的方法
本篇文章主要介紹了Java 是如何利用接口避免函數(shù)回調(diào)的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-02-02JPA如何設(shè)置表名和實(shí)體名,表字段與實(shí)體字段的對(duì)應(yīng)
這篇文章主要介紹了JPA如何設(shè)置表名和實(shí)體名,表字段與實(shí)體字段的對(duì)應(yīng),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11Java設(shè)置session超時(shí)的幾種方式總結(jié)
這篇文章主要介紹了Java設(shè)置session超時(shí)的幾種方式總結(jié)的相關(guān)資料,需要的朋友可以參考下2017-07-07springboot項(xiàng)目打包并部署到Tomcat上及報(bào)錯(cuò)處理方案
這篇文章主要介紹了springboot項(xiàng)目打包并部署到Tomcat上及報(bào)錯(cuò)處理方案,本文給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-08-08