java線程本地變量ThreadLocal詳解
介紹
ThreadLocal作為JDK1.2以來的一個java.lang包下的一個類,在面試和工程中都非常重要,這個類的主要目的是提供線程本地的變量,所以也有很多地方把這個類叫做線程本地變量
從字面理解,這個類為每個線程都創(chuàng)建了一個本地變量,實際上是ThreadLocal為變量在每個線程中都創(chuàng)建了一個副本,使得每個線程都可以訪問自己內(nèi)部的副本變量
通常提到多線程,都會考慮變量同步的問題,但是ThreadLocal并不是為了解決多線程共享變量同步的問題,而是為了讓每個線程的變量不互相影響,相當(dāng)于線程之間操縱的都是變量的副本,自然就不用考慮多線程競爭的問題,也自然沒有性能損耗
使用方式
先來看常用的這幾個方法
public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }
顯而易見,get()方法獲取線程擁有的副本值,set()方法進(jìn)行設(shè)值,remove()方法移除,initialValue()進(jìn)行變量初始化,我們先來看下面這個實例,同時體會一下應(yīng)用場景
public class Demo {
public static ThreadLocal<Integer> threadLocal = null;
public static void main(String[] args) {
threadLocal = new ThreadLocal<Integer>() {
/**
* 通過重寫該方法來初始化ThreadLocal的值
*/
@Override
protected Integer initialValue() {
return 10;
}
};
MyThread t1 = new MyThread(20);
MyThread t2 = new MyThread(30);
t1.start();
// 這里為了描述清晰,省略了try-catch語句塊
t1.join();
t2.start();
}
}
在上述方法中,我們定義并初始化一個ThreadLocal類為10(通過重寫initialValue()方法實現(xiàn)),然后開啟了兩個線程,同時我們這里讓t2線程等待t1線程執(zhí)行完再執(zhí)行
MyThread類詳細(xì)信息如下
class MyThread extends Thread {
private int val = 0;
MyThread(int val) {
this.val = val;
}
@Override
public void run() {
System.out.println(Thread.currentThread() + "-BEFORE-" + Demo.threadLocal.get());
Demo.threadLocal.set(val);
System.out.println(Thread.currentThread() + "-AFTER-" + Demo.threadLocal.get());
}
}
我們通過調(diào)用ThreadLocal對象的get()方法來獲取當(dāng)前的值,然后通過set()方法設(shè)置一個新值(每個線程我們設(shè)置不同的值),然后再通過get()方法來獲取設(shè)置后的值
運(yùn)行結(jié)果如下

重點是圖中標(biāo)注的t2線程變量的初始值,雖然我們在t1線程中修改了變量的值,但是在t2線程中變量值并沒有被改變,這樣就實現(xiàn)了每個線程獨(dú)有的變量
同時,如果一個ThreadLocal對象要在很多地方進(jìn)行復(fù)用時,需要在使用前通過調(diào)用**remove()**方法來將本地變量恢復(fù)到默認(rèn)值
也許有人會問了,我們給每個線程定義自己的私有變量不是也可以實現(xiàn)同樣的操作嗎,理論上當(dāng)然是可行的,但是ThreadLocal遠(yuǎn)比私有變量的形式方便,不僅可以在線程外部進(jìn)行統(tǒng)一的初始化,而且避免在線程內(nèi)部額外設(shè)置變量
原理
點進(jìn)ThreadLocal的源碼中,發(fā)現(xiàn)并沒有存儲變量的字段值,那看來ThreadLocal并不負(fù)責(zé)保存變量,我們只能從方法下手
先看initial()方法,畢竟我們的變量默認(rèn)初始值就是在這個方法中設(shè)置,如下
protected T initialValue() {
return null;
}
我們在每次創(chuàng)建ThreadLocal都要重寫這個方法,那么這個方法到底在哪調(diào)用呢,我們點進(jìn)get()方法源碼中,如下
public T get() {
// 獲取當(dāng)前線程
Thread t = Thread.currentThread();
// 獲取當(dāng)前線程的map
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
// 如果map為空則進(jìn)行創(chuàng)建
return setInitialValue();
}
有點眉頭了,我們發(fā)現(xiàn)這里獲取了一個ThreadLocalMap對象,所以會想到有可能是通過讓線程與變量作一個KV表,來實現(xiàn)每個線程擁有自己獨(dú)有的變量
我們點進(jìn)getMap(t)方法中,發(fā)現(xiàn)返回了線程t的一個threadLocals屬性,這是Thread類的一個字段:
ThreadLocal.ThreadLocalMap threadLocals = null;
這是一個由ThreadLocal類維護(hù)的屬性,Thread的任何方法都沒有對這個字段進(jìn)行修改操作,而這個ThreadLocalMap本身又是ThreadLocal的一個內(nèi)部類,可以把它理解成一個Map(雖然這個類沒有繼承Map接口)
同時要注意,在ThreadLocalMap對象中的Entry對象(鍵值對),繼承了一個ThreadLocaMap的弱引用,如下
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
也就是說,當(dāng)ThreadLocal被置為空時,Entry中的Key則會在下一次YGC中被回收
我們還是沒有看到initialValue()方法,別急,點進(jìn)setInitialValue()方法,也就是如果在get()方法中檢測到map為空時調(diào)用的方法,如下
private T setInitialValue() {
// 我們設(shè)定的初始值
T value = initialValue();
// 當(dāng)前線程
Thread t = Thread.currentThread();
// 再檢查一次是否為空
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
現(xiàn)在基本操作我們都清楚了,set()方法和initialValue()幾乎完全一致,remove()方法則是普通地移除了一個KV鍵值對(K為當(dāng)前線程),這里均不再列出,如果感興趣可以自行查看
注意事項
1.臟數(shù)據(jù)
從上面的分析可以看出,ThreadLocal是和Thread綁定的,每一個Thread對應(yīng)一個value,如果沒有在使用結(jié)束后調(diào)用remove()方法,就會在下一次重用時讀到臟數(shù)據(jù)(針對同一個線程而言),尤其是使用線程池的場景(線程池中的線程經(jīng)常會復(fù)用)
2.內(nèi)存泄露
一般在使用時都會將ThreadLocal設(shè)置為靜態(tài)字段,這時候當(dāng)線程執(zhí)行完成后,KV中的V是不會自動回收的,所以要在使用完后及時調(diào)用remove()方法清理
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
詳解Spring的兩種代理方式:JDK動態(tài)代理和CGLIB動態(tài)代理
這篇文章主要介紹了詳解Spring的兩種代理方式:JDK動態(tài)代理和CGLIB動態(tài)代理,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-04-04
Java中計算集合中元素的出現(xiàn)次數(shù)統(tǒng)計
本文主要介紹了Java中計算集合中元素的出現(xiàn)次數(shù)統(tǒng)計,使用Collections類配合HashMap來統(tǒng)計和java lamb 計算這兩種方式,具有一定的參考價值,感興趣可以了解一下2024-02-02
Spring?Boot中使用Spring?Retry重試框架的操作方法
這篇文章主要介紹了Spring?Retry?在SpringBoot?中的應(yīng)用,介紹了RetryTemplate配置的時候,需要設(shè)置的重試策略和退避策略,需要的朋友可以參考下2022-04-04
MybatisPlus查詢條件空字符串和NULL問題背景分析
文章詳細(xì)分析了MybatisPlus在處理查詢條件時,空字符串和NULL值的問題,MP 3.3.0及以上版本提供了多種解決方法,包括在Bean屬性上使用注解、全局配置等,推薦使用全局配置的方式來解決這個問題,以避免在SQL查詢中出現(xiàn)不必要的空字符串條件,感興趣的朋友跟隨小編一起看看吧2025-03-03
Java8時間api之LocalDate/LocalDateTime的用法詳解
在項目中,時間的使用必不可少,而java8之前的時間api?Date和Calander等在使用上存在著很多問題,于是,jdk1.8引進(jìn)了新的時間api-LocalDateTime,本文就來講講它的具體使用吧2023-05-05
Spring Boot2開發(fā)之Spring Boot整合Shiro兩種詳細(xì)方法
這篇文章主要介紹了Spring Boot2開發(fā)之Spring Boot整合Shiro詳細(xì)方法,需要的朋友可以參考下2020-03-03
關(guān)于swagger配置及踩坑@Api參數(shù)postion無效解決接口排序問題
這篇文章主要介紹了關(guān)于swagger配置及踩坑@Api參數(shù)postion無效解決接口排序問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-06-06

