Java中的ThreadLocal線程變量詳解
ThreadLocal 簡介
介紹
ThreadLocal叫做線程變量,意思是在ThreadLocal中填充的變量屬于當(dāng)前線程,該變量對其他線程而言是隔離的,它是用來提供線程內(nèi)部的局部變量。這種變量在多線程環(huán)境下訪問時能保證各個線程的變量相對獨立于其他線程內(nèi)的變量。
用一句話來概括:提供線程局部變量,一個局部變量,在多線程中,分別有獨立的值。
特點
- 每個線程都有自己的線程局部變量,且只能訪問自己的,不能訪問其他線程的
- ThreadLocal變量通常被private static修飾,當(dāng)一個線程銷毀(結(jié)束)時,其所有的ThreadLocal變量都會被回收
圖形解析
- 每個Thread線程內(nèi)部都有一個Map(ThreadLocalMap)
- ThreadLocalMap里面存儲ThreadLocal對象(key)和線程的變量值(value)
- Thread內(nèi)部的ThreadLocalMap是由ThreadLocalMap維護(hù)的,由ThreadLocal負(fù)責(zé)向ThreadLocalMap獲取和設(shè)置線程值
- 對于不同的線程,每次獲取值時,別的線程并不能獲取到當(dāng)前線程值,擁有線程隔離,互不干擾
ThreadLocal實現(xiàn)線程隔離基本原理
- ThreadLocal本身不存放數(shù)據(jù),使用線程Thread中的threadLocals屬性,(ThreadLocal是Thread中屬性threadLocals的管理者),threadLocals屬性對應(yīng)在ThreadLocal中定義的ThreadLocalMap對象。
- 在調(diào)用ThreadLocal的set()方法時,會將自身的引用this作為key,用戶傳入的值作為value存入ThreadLocalMap中。這樣每個線程的讀寫操作都是基于線程本身的一個私有值,線程之間的數(shù)據(jù)是相互隔離的,互不影響。
ThreadLoca常用的API方法
- get():返回當(dāng)前線程的ThreadLoca變量的值,如果當(dāng)前線程沒有該 變量就初始化為null返回,如果ThreadLocals屬性沒有就被創(chuàng)建,就新建一個ThreadLocalMap,并創(chuàng)建一個ThreadLocal變量存入map中
- set(T value):將當(dāng)前線程的ThreadLocal變量副本中的值指定為value
- remove():移除當(dāng)前線程的ThreadLocal變量
ThreadLocal源碼解析
get()
public T get() { // 獲取到當(dāng)前線程 Thread t = Thread.currentThread(); // 從當(dāng)前線程t中獲取thredLocals屬性 ThreadLocalMap map = getMap(t); //如果不為null,就說明已經(jīng)初始化過,存在值 if (map != null) { // 獲取當(dāng)前ThreadLocal對象為key值(如果不存在,就創(chuàng)建一個ThreadLocal變量) ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { //如果值不為null,就直接返回 @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } // 如果當(dāng)前線程的threadlocals沒有被創(chuàng)建,就調(diào)用setInitialValue()創(chuàng)建 return setInitialValue(); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } private T setInitialValue() { // value賦值為初始化方法值,默認(rèn)為null T value = initialValue(); // 獲取到當(dāng)前線程 Thread t = Thread.currentThread(); // 獲取當(dāng)前線程t的threadlocals屬性,也就是map集合 ThreadLocalMap map = getMap(t); // 如果map不為null,進(jìn)行賦值 if (map != null) map.set(this, value); else //map為null,創(chuàng)建當(dāng)前線程t的map并賦值 createMap(t, value); return value; }
set()
public void set(T value) { // 獲取到當(dāng)前線程 Thread t = Thread.currentThread(); // 獲取到當(dāng)前線程的threadLocals值,即map值 ThreadLocalMap map = getMap(t); // 如果map不等于null,添加值 if (map != null) map.set(this, value); // 如果等于null,創(chuàng)建當(dāng)前線程t的map,并添加值 else createMap(t, value); }
remove()
public void remove() { // 獲取當(dāng)前線程的threadLocals值,即map集合 ThreadLocalMap m = getMap(Thread.currentThread()); // map不為null就將ThreadLocal變量(this)移除 if (m != null) m.remove(this); } }
適用場景
- 每個線程需要有自己的獨立實例
- 實例需要在多個方法中共享,但不希望被多線程共享
場景舉例:用來存儲用戶信息,數(shù)據(jù)庫連接,數(shù)據(jù)夸層傳遞(由service1傳到service2),解決線程安全問題等等
存在問題
ThreadLoal可能會造成內(nèi)存泄漏,主要原因是線程復(fù)用。
由于ThreadLocal對象是弱引用,如果沒有外部的強(qiáng)引用指向,會導(dǎo)致ThreadLoca對象被回收,Entry中的key變成空,此時如果value沒有外部引用指向,value就永遠(yuǎn)訪問不到了,(按理應(yīng)該被GC回收,但是由于Entry對象table數(shù)組還在強(qiáng)引用value)此時會發(fā)生內(nèi)存泄漏,value成為無法訪問但又永遠(yuǎn)無法回收的對象(除非線程被銷毀,但是處于系統(tǒng)性能考慮,線程不宜頻繁的創(chuàng)建和銷毀,經(jīng)常適用線程池,這樣線程的生命周期變大,內(nèi)存泄漏的影響也就會變大)
總結(jié):threadLocals對象中的Entry對象不再使用后,如果沒有及時清除Entry對象,而程序自身也無法通過垃圾回收機(jī)制自動清除,那么就可能會造成內(nèi)存泄漏。
解決內(nèi)存泄漏方案
- 使用完ThreadLocal變量以后,應(yīng)該盡快調(diào)用remove方法,將其從ThreadLocalMap中清除,以便讓垃圾回收器回收他們。
- 將ThreadLocal變量定義成private static類型,并且咋使用完以后手動清除,以避免線程重用時引起的內(nèi)存泄漏問題
- 不要在線程池中使用ThreadLocal變量,如果必須使用,應(yīng)該在使用完以后手動清理。
內(nèi)存泄漏
內(nèi)存泄漏指得是程序中存在某些對象或資源沒有妥善的釋放。
導(dǎo)致這些對象或資源一直占用著內(nèi)存,從而無法被回收,隨著時間推移,這些未釋放的對象或資源會越來越多,最終耗盡系統(tǒng)內(nèi)存資源,導(dǎo)致系統(tǒng)崩潰。
常見的內(nèi)存泄漏
- 對象被創(chuàng)建以后,沒有及時被銷毀,成為垃圾對象
- 沒有正確關(guān)閉IO資源
- 緩存沒有被清空
- 形態(tài)集合類對象未刪除引用
- 單列模式下對象未及時釋放
內(nèi)存溢出
內(nèi)存溢出是指程序在申請內(nèi)存時,無法獲得足夠的內(nèi)存空間,導(dǎo)致程序無法正常運(yùn)行。
通常情況下,當(dāng)程序需要使用的內(nèi)存超過系統(tǒng)能提供的內(nèi)存時,就會發(fā)生內(nèi)存溢出的情況。
常見的內(nèi)存溢出
- 堆內(nèi)存溢出:由于創(chuàng)建了過多的對象或者某些對象太大,導(dǎo)致堆內(nèi)存不足。
- 棧內(nèi)存溢出:由于方法調(diào)用過多或者某些方法的遞歸調(diào)用層數(shù)過多,導(dǎo)致棧內(nèi)存不足
- 永久代內(nèi)存溢出:由于創(chuàng)建了過多的類或者字符串,導(dǎo)致永久代內(nèi)存不足。
Java中四種引用類型
引用關(guān)系的強(qiáng)弱關(guān)系:強(qiáng)引用>軟引用>弱引用>虛引用
強(qiáng)引用
如果一個對象具有強(qiáng)引用,它就不會被垃圾回收器回收。即使當(dāng)前內(nèi)存空間不足,JVM也不回回收它,而是拋出OutOfMemoryError錯誤,使程序終止異常,如果想中斷強(qiáng)引用和某個對象的關(guān)聯(lián),可以顯式的將引用類型賦值為null,這樣一來,JVM在合適的時間就會回收改對象。
軟引用
在使用軟引用時,如果內(nèi)存的空間足夠,軟引用就能繼續(xù)被使用,而不會被垃圾回收器回收,只有在內(nèi)存不足時,軟引用才會被垃圾回收器回收。(軟引用可以用來實現(xiàn)內(nèi)存敏感的高速緩存,比如網(wǎng)頁緩存,圖片緩存等。使用軟引用能防止內(nèi)存泄漏,增強(qiáng)程序的健壯性)
弱引用
具有弱引用的對象擁有的生命周期更短。當(dāng)jvm進(jìn)行垃圾回收器回收,一旦發(fā)現(xiàn)弱引用對象,無論當(dāng)前內(nèi)存是否充足,都會將弱引用進(jìn)行回收
虛引用
虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。
注意點
虛引用必須和引用隊列聯(lián)合使用,當(dāng)垃圾回收器準(zhǔn)備回收一個對象時,如果發(fā)現(xiàn)它還有虛引用,就會在回收對象內(nèi)存之前,把這個虛引用加入到與之關(guān)聯(lián)的引用隊列之中。(其它引用是被JVM回收后才被加入引用隊列中的,由于這個機(jī)制,所以虛引用大多被用于引用銷毀前工作??梢允褂迷趯ο箐N毀前的一些操作,比如資源釋放)
ThreadLocal示例
示例1
public class ThreadLocalDemo1 { private static ThreadLocal<String> threadLocal1 = new ThreadLocal<>(); private static ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>(); /** * 運(yùn)行 count個線程,每個線程持有自己獨有的 String類型編號 */ public void startThreadArray(int count) { Thread[] runs = new Thread[count]; for (int i = 0; i < runs.length; i++) { // 賦值編號id new ThreadDemo1(i).start(); } } /** * 線程類: */ public static class ThreadDemo1 extends Thread { /** * 編號id */ private int codeId; public ThreadDemo1(int codeId) { this.codeId = codeId; } @Override public void run() { String threadName = Thread.currentThread().getName(); threadLocal1.set("threadLocal1賦值,線程_" + codeId); if (codeId == 2) { //如果是線程2,設(shè)置 threadLocal2變量,值乘以5 threadLocal2.set(codeId * 5); } System.out.println(threadName + " -》 threadLocal1 " + threadLocal1.get()); System.out.println(threadName + " -》 threadLocal2 " + threadLocal2.get()); // 使用完移除,help GC threadLocal1.remove(); threadLocal2.remove(); } } public static void main(String[] args) { ThreadLocalDemo1 useDemo = new ThreadLocalDemo1(); // 啟動3個線程 useDemo.startThreadArray(3); } }
每個線程會獲取到屬于自己的線程值,不會有任何的錯亂
示例2
public class ThreadLocalDemo2 { /** * 初始化 num值。使用時,先通過get方法獲取。 */ public static ThreadLocal<ThreadLocalDemo2.Number> threadLocalValue = new ThreadLocal<ThreadLocalDemo2.Number>() { @Override protected Number initialValue() { return new Number(0); } }; /** * 數(shù)據(jù)類 */ private static class Number { public Number(int num) { this.num = num; } private int num; public int getNum() { return num; } public void setNum(int num) { this.num = num; } @Override public String toString() { return "Number [num=" + num + "]"; } } /** * 線程類: */ public static class ThreadDemo2 extends Thread { @Override public void run() { // 如果沒有初始化,注意NPE。 // static修飾的 number時,注釋掉這句 Number number = threadLocalValue.get(); //每個線程計數(shù)加隨機(jī)數(shù) Random r = new Random(); number.setNum(number.getNum() + r.nextInt(100)); //將其存儲到ThreadLocal中 threadLocalValue.set(number); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } //打印保存的隨機(jī)值 System.out.println(Thread.currentThread().getName() + " -》 " + threadLocalValue.get().getNum()); threadLocalValue.remove(); System.out.println(Thread.currentThread().getName() + " remove方法之后 -》 " + threadLocalValue.get().getNum()); } } public static void main(String[] args) { // 啟動5個線程 for (int i = 0; i < 5; i++) { new ThreadDemo2().start(); } } }
每個線程可以通過initialValue方法初始化變量值。
到此這篇關(guān)于Java中的ThreadLocal線程變量詳解的文章就介紹到這了,更多相關(guān)Java中的ThreadLocal內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用maven-assembly-plugin如何將system 依賴范圍的jar以class 方式
這篇文章主要介紹了使用maven-assembly-plugin如何將system 依賴范圍的jar以class 方式打包進(jìn) jar包中,本文給大家分享完美解決思路,結(jié)合實例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2023-06-06SpringBoot使用PageHelper插件實現(xiàn)Mybatis分頁效果
這篇文章主要介紹了SpringBoot使用PageHelper插件實現(xiàn)Mybatis分頁效果,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作有一定的參考借鑒價值,需要的朋友可以參考下2024-02-02springboot創(chuàng)建多module項目的實例
這篇文章主要介紹了springboot創(chuàng)建多module項目的實例代碼,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-02-02mybatis向數(shù)據(jù)庫里插入記錄后自動返回記錄ID問題
本文介紹了在接手項目時,對一個業(yè)務(wù)處理邏輯進(jìn)行重構(gòu)和性能優(yōu)化的經(jīng)歷,作者提到,性能問題可能是導(dǎo)致bug的一個重要原因,作者提到,在以前的.NET項目中,插入記錄后系統(tǒng)會自動刷新實體類,為其中的主鍵ID賦值,而SpringBoot項目mybatis也可以通過指定主鍵來優(yōu)化代碼2025-01-01Prometheus 入門教程之SpringBoot 實現(xiàn)自定義指標(biāo)監(jiān)控
這篇文章主要介紹了Prometheus 入門教程之SpringBoot 實現(xiàn)自定義指標(biāo)監(jiān)控,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-12-12SpringMVC中redirect重定向(帶參數(shù))的3種方式
Spring MVC中做form表單功能提交時,防止用戶客戶端后退或者刷新時重復(fù)提交問題,需要在服務(wù)端進(jìn)行重定向跳轉(zhuǎn),本文主要介紹了SpringMVC中redirect重定向(帶參數(shù))的3種方式,感興趣的可以了解一下2024-07-07Java中正則表達(dá)式的語法以及matches方法的使用方法
正則表達(dá)式(Regular Expression)是一門簡單語言的語法規(guī)范,是強(qiáng)大、便捷、高效的文本處理工具,這篇文章主要給大家介紹了關(guān)于Java中正則表達(dá)式的語法以及matches方法的使用方法,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-05-05