Java中的ThreadLocal線程變量詳解
ThreadLocal 簡介
介紹
ThreadLocal叫做線程變量,意思是在ThreadLocal中填充的變量屬于當(dāng)前線程,該變量對其他線程而言是隔離的,它是用來提供線程內(nèi)部的局部變量。這種變量在多線程環(huán)境下訪問時能保證各個線程的變量相對獨(dú)立于其他線程內(nèi)的變量。
用一句話來概括:提供線程局部變量,一個局部變量,在多線程中,分別有獨(dú)立的值。
特點(diǎn)
- 每個線程都有自己的線程局部變量,且只能訪問自己的,不能訪問其他線程的
- 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實(shí)現(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);
}
}適用場景
- 每個線程需要有自己的獨(dú)立實(shí)例
- 實(shí)例需要在多個方法中共享,但不希望被多線程共享
場景舉例:用來存儲用戶信息,數(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)存不足時,軟引用才會被垃圾回收器回收。(軟引用可以用來實(shí)現(xiàn)內(nèi)存敏感的高速緩存,比如網(wǎng)頁緩存,圖片緩存等。使用軟引用能防止內(nèi)存泄漏,增強(qiáng)程序的健壯性)
弱引用
具有弱引用的對象擁有的生命周期更短。當(dāng)jvm進(jìn)行垃圾回收器回收,一旦發(fā)現(xiàn)弱引用對象,無論當(dāng)前內(nèi)存是否充足,都會將弱引用進(jìn)行回收
虛引用
虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。
注意點(diǎn)
虛引用必須和引用隊(duì)列聯(lián)合使用,當(dāng)垃圾回收器準(zhǔn)備回收一個對象時,如果發(fā)現(xiàn)它還有虛引用,就會在回收對象內(nèi)存之前,把這個虛引用加入到與之關(guān)聯(lián)的引用隊(duì)列之中。(其它引用是被JVM回收后才被加入引用隊(duì)列中的,由于這個機(jī)制,所以虛引用大多被用于引用銷毀前工作。可以使用在對象銷毀前的一些操作,比如資源釋放)
ThreadLocal示例
示例1
public class ThreadLocalDemo1 {
private static ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
private static ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
/**
* 運(yùn)行 count個線程,每個線程持有自己獨(dú)有的 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();
//每個線程計(jì)數(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)文章
JAVA實(shí)現(xiàn)二維碼生成加背景圖代碼實(shí)例
這篇文章主要介紹了JAVA實(shí)現(xiàn)二維碼生成加背景圖代碼實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-12-12
SpringCloud中使用Sentinel實(shí)現(xiàn)限流的實(shí)戰(zhàn)
限流在很多地方都可以使用的到,本篇博客將介紹如何使用SpringCloud中使用Sentinel實(shí)現(xiàn)限流,從而達(dá)到服務(wù)降級的目的,感興趣的可以了解一下2022-01-01
Java后端SSM框架圖片上傳功能實(shí)現(xiàn)方法解析
這篇文章主要介紹了Java后端SSM框架圖片上傳功能實(shí)現(xiàn)方法解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-06-06
淺析Java中Map與HashMap,Hashtable,HashSet的區(qū)別
HashMap和Hashtable兩個類都實(shí)現(xiàn)了Map接口,二者保存K-V對(key-value對);HashSet則實(shí)現(xiàn)了Set接口,性質(zhì)類似于集合2013-09-09

