引言
ThreadLocal的官方API解釋為:
“該類提供了線程局部 (thread-local) 變量。這些變量不同于它們的普通對(duì)應(yīng)物,因?yàn)樵L問(wèn)某個(gè)變量(通過(guò)其 get 或 set 方法)的每個(gè)線程都有自己的局部變量,它獨(dú)立于變量的初始化副本。ThreadLocal 實(shí)例通常是類中的 private static 字段,它們希望將狀態(tài)與某一個(gè)線程(例如,用戶 ID 或事務(wù) ID)相關(guān)聯(lián)。”
大概的意思有兩點(diǎn):
- 1、ThreadLocal提供了一種訪問(wèn)某個(gè)變量的特殊方式:訪問(wèn)到的變量屬于當(dāng)前線程,即保證每個(gè)線程的變量不一樣,而同一個(gè)線程在任何地方拿到的變量都是一致的,這就是所謂的線程隔離。
- 2、如果要使用ThreadLocal,通常定義為private static類型,在我看來(lái)最好是定義為private static final類型。
什么是ThreadLocal變量
ThreadLoal 變量,線程局部變量,同一個(gè) ThreadLocal 所包含的對(duì)象,在不同的 Thread 中有不同的副本。這里有幾點(diǎn)需要注意:
因?yàn)槊總€(gè) Thread 內(nèi)有自己的實(shí)例副本,且該副本只能由當(dāng)前 Thread 使用。這是也是 ThreadLocal 命名的由來(lái)。
既然每個(gè) Thread 有自己的實(shí)例副本,且其它 Thread 不可訪問(wèn),那就不存在多線程間共享的問(wèn)題。
ThreadLocal 提供了線程本地的實(shí)例。它與普通變量的區(qū)別在于,每個(gè)使用該變量的線程都會(huì)初始化一個(gè)完全獨(dú)立的實(shí)例副本。ThreadLocal 變量通常被private static修飾。當(dāng)一個(gè)線程結(jié)束時(shí),它所使用的所有 ThreadLocal 相對(duì)的實(shí)例副本都可被回收。
總的來(lái)說(shuō),ThreadLocal 適用于每個(gè)線程需要自己獨(dú)立的實(shí)例且該實(shí)例需要在多個(gè)方法中被使用,也即變量在線程間隔離而在方法或類間共享的場(chǎng)景。
兩大使用場(chǎng)景——ThreadLocal的用途
- 1、每個(gè)線程需要一個(gè)獨(dú)享的對(duì)象(通常是工具類,典型需要使用的類有SimpleDateFormat和Random)
- 2、每個(gè)線程內(nèi)需要保存全局變量(例如在攔截器中獲取用戶信息),可以讓不同方法直接使用,避免參數(shù)傳遞的麻煩
每個(gè)線程需要一個(gè)獨(dú)享的對(duì)象
- 每個(gè)Thread內(nèi)有自己的實(shí)例副本,不共享
/**
* @Description: 1000個(gè)打印日期的任務(wù),用線程池來(lái)執(zhí)行 使用ThreadLocal來(lái)解決線程安全問(wèn)題
*/
public class ThreadLocalTest6 {
public static ExecutorService executorService = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
final int seconds = i;
executorService.submit(new Runnable() {
public void run() {
String date = new ThreadLocalTest6().date(seconds);
System.out.println(date);
}
});
// executorService.shutdown();
}
}
public String date(int seconds){
//參數(shù)的單位是毫秒,從1970.1.1 00:00:00開始計(jì)時(shí)
Date date = new Date(1000 * seconds);
SimpleDateFormat simpleDateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
return simpleDateFormat.format(date);
}
}
class ThreadSafeFormatter{
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
//初始化SimpleDateFormat
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
每個(gè)線程內(nèi)需要保存全局變量
當(dāng)前用戶信息需要被線程內(nèi)所有方法共享
-
比較繁瑣的解決方案是把user作為參數(shù)層層傳遞,從service-1()傳遞到service-2(),在從service-2()傳遞到service-3(),以此類推,但是這樣做會(huì)導(dǎo)致代碼冗余且不容易維護(hù)
每個(gè)線程內(nèi)需要保存全局變量,可以讓不同方法直接使用,避免參數(shù)傳遞的麻煩
-
在此基礎(chǔ)上可以演進(jìn),使用UserMap
-
當(dāng)多線程同時(shí)工作時(shí),我們需要保證線程安全,可以使用synchronized,也可以使用ConcurrentHashMap,但無(wú)論使用什么,都會(huì)對(duì)性能有所影響
更好的辦法就是使用ThreadLocal,這樣無(wú)需synchronized,可以在不影響性能的情況下,也無(wú)需層層傳遞參數(shù),就可以達(dá)到保存當(dāng)前線程對(duì)應(yīng)的用戶信息的目的
方法
- 使用ThreadLocal保存一些業(yè)務(wù)內(nèi)容(用戶權(quán)限信息,從用戶系統(tǒng)獲取到的用戶名、userID等)
- 這些信息在同一個(gè)線程內(nèi)相同,但是不同的線程使用的業(yè)務(wù)內(nèi)容是不同的
- 在線程生命周期內(nèi),都通過(guò)這個(gè)靜態(tài)ThreadLocal實(shí)例的get()方法取得自己set進(jìn)去的那個(gè)對(duì)象,從而避免了將這個(gè)對(duì)象(例如user對(duì)象)作為參數(shù)傳遞的麻煩
- 強(qiáng)調(diào)的是同一個(gè)請(qǐng)求(同一個(gè)線程內(nèi))不同方法間的共享
-
不需要重寫initialValue()方法,但是必須手動(dòng)調(diào)用set()方法
/**
* @author: huangyibo
* @Date: 2020/2/15 16:03
* @Description: 演示ThreadLocal用法2,避免傳遞參數(shù)的麻煩
*/
public class ThreadLocalDemo1 {
public static void main(String[] args) {
Service1 service1 = new Service1();
service1.process();
}
}
class Service1{
public void process(){
User user = new User("小明");
UserContextHolder.holder.set(user);
new Service2().process();
}
}
class Service2{
public void process(){
User user = UserContextHolder.holder.get();
System.out.println("service2:"+user.name);
new Service3().process();
}
}
class Service3{
public void process(){
User user = UserContextHolder.holder.get();
System.out.println("service3:"+user.name);
UserContextHolder.holder.remove();//使用完ThreadLocal之后調(diào)用remove()方法,避免內(nèi)存泄漏
}
}
class UserContextHolder{
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User{
String name;
public User(String name) {
this.name = name;
}
}
ThreadLocal的兩個(gè)作用總結(jié)
- 1、讓某個(gè)需要用到的對(duì)象在線程間隔離(每個(gè)線程都有自己獨(dú)立的對(duì)象)
- 2、在任何方法中都可以輕松獲取到該對(duì)象
- 3、根據(jù)共享對(duì)象的生成時(shí)機(jī)不同,選擇initialValue或set來(lái)保存對(duì)象
場(chǎng)景一:initialValue
- 在ThreadLocal第一次get的時(shí)候把對(duì)象給初始化出來(lái),對(duì)象的初始化時(shí)機(jī)可以由我們控制
場(chǎng)景二:set
- 如果需要保存到ThreadLocal里的對(duì)象的生成時(shí)機(jī)不由我們隨意控制,例如攔截器生成的用戶信息,用ThreadLocal.set直接放到我們的ThreadLocal中去,以便后續(xù)使用
使用ThreadLocal帶來(lái)的好處
- 線程安全
- 不需要加鎖,提高執(zhí)行效率
- 更高效的利用內(nèi)存,節(jié)省開銷。相比與每個(gè)任務(wù)都新建一個(gè)SimpleDateFormat,顯然ThreadLocal可以節(jié)省內(nèi)存和開銷
- 避免傳參的繁瑣。無(wú)論是場(chǎng)景一的工具類,還是場(chǎng)景二的用戶名,都可以在任何地方直接通過(guò)ThreadLocal拿到,再也不需要每次都傳同樣的參數(shù),ThreadLocal使得代碼耦合度更低,更優(yōu)雅
ThreadLocal原理
首先 ThreadLocal 是一個(gè)泛型類,保證可以接受任何類型的對(duì)象。
因?yàn)橐粋€(gè)線程內(nèi)可以存在多個(gè) ThreadLocal 對(duì)象,所以其實(shí)是 Thread 內(nèi)部維護(hù)了一個(gè) Map ,這個(gè) Map 不是直接使用的 HashMap ,而是 ThreadLocal 實(shí)現(xiàn)的一個(gè)叫做 ThreadLocalMap 的靜態(tài)內(nèi)部類。而我們使用的 get()、set() 方法其實(shí)都是調(diào)用了這個(gè)ThreadLocalMap類對(duì)應(yīng)的 get()、set() 方法。例如下面的 set 方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocal主要方法
- protected T initialValue():初始化
- 該方法會(huì)返回當(dāng)前線程對(duì)應(yīng)的“初始值”,這是一個(gè)延遲加載的方法,只有在調(diào)用get的時(shí)候才會(huì)觸發(fā)
- 當(dāng)線程第一次調(diào)用get方法訪問(wèn)變量時(shí),調(diào)用此方法,如果線程先前調(diào)用了set方法,在這種請(qǐng)求下,不會(huì)為線程調(diào)用本initialValue方法
- 通常,每個(gè)線程最多調(diào)用一次此方法,但如果已經(jīng)調(diào)用了remove()后,再調(diào)用get(),則可以再次調(diào)用此方法
- 如果不重寫該方法,這個(gè)方法會(huì)返回null,一般使用匿名內(nèi)部類的方法來(lái)重寫initialValue()方法,以便在后續(xù)使用中可以初始化副本對(duì)象
- public void set(T value) :為這個(gè)線程設(shè)置一個(gè)新值
- public T get() :得到這個(gè)線程對(duì)應(yīng)的value。如果是首次調(diào)用get(),則會(huì)調(diào)用initialValue()方法來(lái)得到這個(gè)值
- public void remove():刪除對(duì)應(yīng)線程的值
ThreadLocal主要方法源碼解析
- public T get() :get方法是先取出當(dāng)前線程的ThreadLocalMap,然后調(diào)用map.getEntry方法,把本ThreadLocal的引用作為參數(shù)傳入,取出map中屬于本ThreadLocal的value
- 注意這個(gè)map以及map中的key和value都是保存在線程中的,而不是保存在ThreadLocal中
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
- public void set(T value) :和setInitialValue()方法很類似
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
- public void remove()方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
ThreadLocalMap
- ThreadLocalMap類,也就是Thread.threadLocals
- ThreadLocalMap類是每個(gè)線程Thread類里面的變量,里面最重要的是一個(gè)鍵值對(duì)數(shù)組Entry[] table,可以認(rèn)為是一個(gè)Map,鍵值對(duì):
- 鍵:這個(gè)ThreadLocal
- 值:實(shí)際需要的成員變量,比如User或者SimpleDateFormat
- ThreadLocalMap雖然類似HashMap但是處理處理Hash沖突略有不同
-
HashMap
- ThreadLocalMap采用的是線性探測(cè)法,也就是如果發(fā)生Hash沖突,就繼續(xù)尋找下一個(gè)空位,而不是采用鏈表拉鏈
-
ThreadLocal兩種使用場(chǎng)景殊途同歸
- 通過(guò)源碼分析可以看出,setInitialValue和直接set最后都是利用map.set()方法來(lái)設(shè)置值
- 也就是說(shuō),最后都會(huì)對(duì)應(yīng)到ThreadLocalMap的一個(gè)Entry,只是起點(diǎn)和入口不一樣
ThreadLocal注意點(diǎn)
內(nèi)存泄漏問(wèn)題
內(nèi)存泄漏:某個(gè)對(duì)象不再有用,但是占用的內(nèi)存確不能被回收。
Key的泄漏
ThreadLocalMap中的Entry繼承自WeakReference,是弱引用,弱引用的特點(diǎn)是,如果這個(gè)對(duì)象只被弱引用關(guān)聯(lián)(沒(méi)有任何強(qiáng)引用關(guān)聯(lián)),那么在下一次垃圾回收的時(shí)候必然會(huì)被清理掉。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Value的泄漏
- ThreadLocalMap的每個(gè)Entry都是一個(gè)對(duì)Key的弱引用,同時(shí)每個(gè)Entry都包含了一個(gè)對(duì)Value的強(qiáng)引用
- 正常情況下,當(dāng)線程終止,保存在ThreadLocal里的Value會(huì)被垃圾回收,因?yàn)闆](méi)有任何強(qiáng)引用關(guān)聯(lián)
-
但是,如果線程始終不終止(比如線程需要保持很久或者使用線程池的時(shí)候),那么Key對(duì)應(yīng)的Value就不能被回收,因?yàn)橛幸韵碌恼{(diào)用鏈:
- 因?yàn)閂alue和Thread之間還存在這個(gè)強(qiáng)引用調(diào)用鏈,所以導(dǎo)致value無(wú)法被回收,就可會(huì)出現(xiàn)OOM
- JDK已經(jīng)考慮到了這個(gè)問(wèn)題,所以在set()、remove()、rehash()方法中會(huì)掃描key為null的Entry,并把對(duì)應(yīng)的value設(shè)置為null,這樣value對(duì)象就可以被回收
- 但是如果一個(gè)ThreadLocal不被使用,那么實(shí)際上set()、remove()、rehash()等方法也不會(huì)被調(diào)用,并且線程又不停止,那么調(diào)用鏈就一直存在,那么也就導(dǎo)致了value的內(nèi)存泄漏
如何避免內(nèi)存泄漏(阿里代碼規(guī)約)
- 調(diào)用remove()方法,就會(huì)刪除對(duì)應(yīng)的Entry對(duì)象,可以避免內(nèi)存泄漏,所以使用完ThreadLocal之后,應(yīng)該調(diào)用remove()方法
- 如果使用攔截器獲取用戶信息,那么同樣應(yīng)該使用攔截器在線程請(qǐng)求退出之前將之前保存過(guò)得信息清除掉
空指針異常
- 在進(jìn)行g(shù)et()之前,必須先進(jìn)行set(),否則可能會(huì)報(bào)空指針異常
public class ThreadLocalNPE {
ThreadLocal<Long> threadLocal = new ThreadLocal<Long>();
public void set(){
threadLocal.set(Thread.currentThread().getId());
}
/**
* 這里的返回值使用long的時(shí)候,如果沒(méi)有set()就調(diào)用get()那么會(huì)報(bào)空指針異常,因?yàn)闋可娴讲鹣滢D(zhuǎn)換(將對(duì)象類型轉(zhuǎn)換為基本類型)
* @return
*/
public Long get(){
return threadLocal.get();
}
public static void main(String[] args) {
ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
System.out.println(threadLocalNPE.get());
new Thread(() -> {
threadLocalNPE.set();
long threadId = threadLocalNPE.get();
System.out.println(threadId);
}).start();
}
}
共享對(duì)象
- 如果在每個(gè)線程中ThreadLocal.set()進(jìn)去的東西本來(lái)就是多線程共享的同一個(gè)對(duì)象,比如static對(duì)象,那么多個(gè)線程的ThreadLocal.get()取得的還是這個(gè)共享對(duì)象本身,還是有并發(fā)訪問(wèn)問(wèn)題
如果不使用ThreadLocal就可以解決問(wèn)題,那么就不要強(qiáng)行使用
- 例如在任務(wù)很少的時(shí)候,在局部變量中可以新建對(duì)象就可以解決問(wèn)題,那么就不需要使用到ThreadLocal
優(yōu)先使用框架的支持,而不是自己創(chuàng)造
- 例如在Spring中,如果可以使用RequestContextHolder,那么就不需要自己維護(hù)ThreadLocal,因?yàn)樽约嚎赡軙?huì)忘記調(diào)用remove()方法,造成內(nèi)存泄漏
ThreadLocal在Spring中的實(shí)例分析
- RequestContextHolder和DateTimeContextHolder中,看到里面使用了ThreadLocal
- 每次HTTP請(qǐng)求都對(duì)應(yīng)一個(gè)線程,線程之間相互隔離,這就是ThreadLocal的典型應(yīng)用場(chǎng)景