一、ThreadLocal兩大使用場(chǎng)景
- 每個(gè)線程需要一個(gè)獨(dú)享的對(duì)象
- 每個(gè)線程內(nèi)需要保存全局變量
1) 每個(gè)線程需要一個(gè)獨(dú)享的對(duì)象
- 通常是工具類(線程不安全),典型需要使用的類比如SimpleDateFormat和Random
- ThreadLocal定義為靜態(tài)變量
- 通過重寫initialValue()方法在本地線程第一次獲取對(duì)象時(shí)進(jìn)行創(chuàng)建。
- 本地線程通過threadLocal.get()獲取該對(duì)象。
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @auth Hahadasheng
* @since 2020/10/27
*/
public class ThreadLocalExclusiveObj {
private static final ThreadLocal<SimpleDateFormat> dateFormatLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"));
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
final int index = i;
executor.execute(() -> {
Date date = new Date(1000 * index);
String format = dateFormatLocal.get().format(date);
System.out.println(format);
});
}
executor.shutdown();
}
}
- 拓展:時(shí)間格式化
注意,h和H代表的含義是不一樣的
y = 年(yy或yyyy)
M = 月(MM)
d = 月中的天(dd)
h = 小時(shí)(0-12)(hh)
H = 小時(shí)(0-23)(HH)
m = 時(shí)分(mm)
s = 秒(ss)
S = 毫秒(SSS)
z = 時(shí)區(qū)文本(例如,太平洋標(biāo)準(zhǔn)時(shí)間…)
Z = 時(shí)區(qū),時(shí)間偏移量(例如-0800)
以下是一些模式示例,其中包含每個(gè)模式如何格式化或期望解析日期的示例:
yyyy-MM-dd(2009-12-31)
dd-MM-YYYY(31-12-2009)
yyyy-MM-dd HH:mm:ss(2009-12-31 23:59:59)
HH:mm:ss.SSS(23:59.59.999)
yyyy-MM-dd HH:mm:ss.SSS(2009-12-31 23:59:59.999)
yyyy-MM-dd HH:mm:ss.SSS Z(2009-12-31 23:59:59.999 +0100)
2) 每個(gè)線程內(nèi)需要保存全局變量
- 比如在攔截器中獲取用戶的信息,可以讓不同方法直接使用,避免參數(shù)傳遞的麻煩。
- 在本地線程生命周期內(nèi),通過set/get方法設(shè)置獲取線程獨(dú)占變量,避免參數(shù)到處傳遞。
- 強(qiáng)調(diào)的是同一個(gè)請(qǐng)求內(nèi)(同一個(gè)線程)不同方法間的共享。
- 不需要要重寫initialValue()方法
可以利用共享的Map:使用static的ConcurrentHashMap,把當(dāng)前線程的ID作為key,把user作為value來保存,這樣可以做到線程間的隔離,但是依然有性能影響。使用ThreadLocak就沒有性能影響,內(nèi)部沒有使用synchronized等同步機(jī)制,也無需層層傳遞參數(shù)。
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @auth Hahadasheng
* @since 2020/10/29
*/
public class ThreadLocalShareInThread {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 20; i++) {
final int index = i;
pool.execute(() -> {
User user = new User();
user.setId(String.format("NO.%s", index + 1));
user.setName(String.format("HHDS-%s", index + 1));
user.setGender(index & 1);
user.setAge(index + 10);
UserHolder.holder.set(user);
otherMethod();
});
}
pool.shutdown();
}
public static void otherMethod() {
System.out.println(UserHolder.holder.get());
UserHolder.holder.remove();
}
}
@Getter
@Setter
class User {
private String id;
private String name;
private int gender;
private int age;
@Override
public String toString() {
return "{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", gender=" + gender +
", age=" + age +
'}';
}
}
class UserHolder {
public static final ThreadLocal<User> holder = new ThreadLocal<>();
}
3) 總結(jié)
- 讓某個(gè)需要用到的對(duì)象在線程間隔離,每個(gè)線程都有自己獨(dú)立的對(duì)象
- 在任何方法中都能輕松獲取該對(duì)象。
- initialValue使用場(chǎng)景:
- 在ThreadLocal第一次get的時(shí)候吧對(duì)象給初始化,對(duì)象的初始化時(shí)機(jī)可以由我們控制
- set:
- 如果需要保存到ThreadLocal里面的對(duì)象的生成時(shí)機(jī)不由我們隨意控制,比如攔截器生成的用戶信息,用ThreadLocal.set直接放進(jìn)去即可
4) ThreadLocal帶來的好處
- 達(dá)到線程安全
- 不需要加鎖,提高執(zhí)行效率
- 更高效地利用內(nèi)存,節(jié)省開銷(例如每個(gè)線程持有一個(gè)SimpleDateFormat)。
- 免去傳參的繁瑣
二、ThreadLocal原理
1) Thread與ThreadLocal以及ThreadLocalMap之間的關(guān)系
- 每個(gè)Thread實(shí)例都會(huì)有一個(gè)獨(dú)立的ThreadLocalMap對(duì)象
- ThreadLocalMap中的Entry的key為ThreadLocal對(duì)應(yīng)的引用(弱引用),value則是線程獨(dú)享的對(duì)象

2) 重要方法
1> T initialValue():該方法會(huì)返回當(dāng)前線程對(duì)應(yīng)的“初始值”,延遲加載的方法,只有在調(diào)用get的時(shí)候才會(huì)觸發(fā)。
- 當(dāng)線程第一次使用get方法訪問變量時(shí),將調(diào)用此方法,除非線程先前調(diào)用了set方法,在這種相框下,不會(huì)為線程調(diào)用本initialValue方法。
get內(nèi)部實(shí)現(xiàn)是檢查對(duì)象是否為null,如果為null則執(zhí)行initialValue()方法<重寫該方法后執(zhí)行重寫的方法>,否則直接返回對(duì)象。
3.如果調(diào)用了remove()后,再調(diào)用get(),則可以再次調(diào)用initialValue()方法。
- initialValue()方法默認(rèn)實(shí)現(xiàn)是直接返回一個(gè)null,如果需要獨(dú)享對(duì)象,一般使用匿名內(nèi)部類的方式重寫該方法。
ThreadLocal.withInitial(() -> {... return ...})
2> void set(T t)
- 為這個(gè)線程設(shè)置一個(gè)新值
3> T get()
- 得到線程對(duì)應(yīng)的value。如果是首次調(diào)用get()<之前沒有調(diào)用void set(T t)>,則會(huì)調(diào)用initialValue來得到這個(gè)值。
4> void remoe()
- 刪除線程對(duì)應(yīng)的值。
3) 源碼分析
- get方法是先取出當(dāng)前線程的ThreadLocalMap,然后調(diào)用map.getEntry方法,把本ThreadLocal的引用作為參數(shù)傳入,取出map中屬于本ThreadLocal的value
- 注意,這個(gè)map以及map中的key和value都是保存在線程中的,而不是保存在ThreadLocal中
- initialValue方法:默認(rèn)返回null,可以自定義實(shí)現(xiàn)
- remove方法,只刪除ThreadLocalMap對(duì)應(yīng)本ThreadLocal引用的Entry
4) ThreadLocalMap類
在Thread中以threadLocals作為成員變量
-
ThreadLocalMap類是每個(gè)線程Thread類里面的變量,里面最重要的是一個(gè)鍵值對(duì)數(shù)組Entry[] table,可以認(rèn)為是一個(gè)Map鍵值對(duì)
- 鍵:這個(gè)ThreadLocal
- 值:實(shí)際需要的成員變量
-
ThreadLocalMap類使用上類似HashMap,但是在實(shí)現(xiàn)上略有不同,
- 并沒有實(shí)現(xiàn)Map接口
-
解決沖突
- HashMap解決Hash沖突的思路是鏈表+紅黑樹
- ThreadLocalMap采用的是線性探測(cè)法:如果發(fā)生沖突,就繼續(xù)找下一個(gè)空位置,而不是用拉鏈或者紅黑樹
可以當(dāng)做為一個(gè)Map去理解
5) 兩種使用場(chǎng)景殊途同歸
- setInitialValue和直接set最后都是利用map.set()方法來設(shè)置值。最后都會(huì)對(duì)應(yīng)到ThreadLocalMap的一個(gè)Entry,只不過起點(diǎn)和入口不一樣。
三、ThreadLocal注意點(diǎn)
1) 內(nèi)存泄露
弱引用:如果一個(gè)對(duì)象只被弱引用關(guān)聯(lián)(沒有任務(wù)強(qiáng)引用),那這個(gè)對(duì)象可以被GC垃圾回收
- 內(nèi)存泄露:某個(gè)對(duì)象不再有用,但是占用的內(nèi)存卻不能被回收。
- ThreadLocalMap中Entry的key的是弱引用,不會(huì)導(dǎo)致泄露問題。
- 每個(gè)Entry都包含一個(gè)對(duì)value的強(qiáng)引用。
- 正常情況下,當(dāng)線程終止,保存在ThreadLocalMap里的key, value會(huì)被垃圾回收,沒有任何強(qiáng)引用。
- 如果線程不終止(比如線程需要保持很久),key對(duì)應(yīng)的value就不能被回收,存在如下調(diào)用鏈
- Thread->ThreadLocalMap->Entry(key為null)->Value
- value無法回收,就可能出現(xiàn)OOM
- JDK已經(jīng)考慮到這個(gè)問題,所以在set,remove,rehash方法中會(huì)掃描key為null的Entry并發(fā)對(duì)應(yīng)的value設(shè)置為null,這樣value對(duì)象就可以被回收了。
- 如果一個(gè)ThreadLocal不被使用,那么實(shí)際上set,remove,rehash方法也不會(huì)被調(diào)用,如果同時(shí)
線程又不停止,那么調(diào)用鏈就一直存在,就導(dǎo)致了value的內(nèi)存泄露
2) 避免內(nèi)存泄露(阿里規(guī)約)
- 使用完ThreadLocal之后主動(dòng)調(diào)用remove方法,刪除Entry對(duì)象,避免內(nèi)存泄露。
3) ThreadLocal空指針異常
- 在進(jìn)行g(shù)et之前,必須先set,否則可能會(huì)報(bào)空指針異常?
- 可能是寫的代碼缺陷:包裝類拆箱導(dǎo)致
/**
* @auth Hahadasheng
* @since 2020/10/30
*/
public class ThreadLocalNPE {
private static final ThreadLocal<Long> localId = new ThreadLocal<>();
/**
* 這里在沒有調(diào)用initializeValue以及set的前提下直接調(diào)用get方法,
* 似乎直接返回null,但是卻報(bào)java.lang.NullPointerException
* 是因?yàn)門hreadLocal定義的泛型為包裝類的Long,在方法返回時(shí)拆箱
* 發(fā)現(xiàn)是null,所以報(bào)空指針
*/
public static long get() {
return localId.get();
}
/**
* 而這個(gè)方法則不會(huì)報(bào)錯(cuò)
*/
public static Long get2() {
return localId.get();
}
public static void main(String[] args) {
System.out.println(get2());
System.out.println(get());
}
}
4) 共享對(duì)象
- 如果在每個(gè)線程中ThreadLocal.set進(jìn)去的本來就是多線程共享的同一個(gè)對(duì)象,比如static對(duì)象,那么多個(gè)線程的ThreadLocal.get取得的還是這個(gè)共享對(duì)象本身,還是有并發(fā)問題。
如果可以不需要使用ThreadLocal,則不要進(jìn)行強(qiáng)行使用。
5) 優(yōu)先使用框架的支持,而不是自己創(chuàng)造
- 例如在Spring中,如果可以使用RequestContextHolder,那么就不需要自己維護(hù)ThreadLocal,因?yàn)樽约嚎赡軙?huì)忘記調(diào)用remove()方法等,造成內(nèi)存泄露。
- Spring中DateTimeContextHolder類,使用了ThreadLocal
6)關(guān)于弱引用被GC清理是否可用的疑惑解答
引用的關(guān)系是:Thread -> ThreadLocalMap -> Entity -> 弱引用ThreadLocal 和 數(shù)據(jù),所以:
雖然是弱引用,但是只要其他地方還有普通引用,就不會(huì)被清理,會(huì)一直存在(1.一般在使用的時(shí)候都是定義為靜態(tài)類屬性常量
... static final ThreadLocal<?> ...,為強(qiáng)引用,只要此類不被虛擬機(jī)卸載,則GC不會(huì)回收該對(duì)象,相關(guān)弱引用也不會(huì)被清理;2.線程執(zhí)行產(chǎn)生的棧幀中局部變量表中可能也會(huì)存在該強(qiáng)引用)。
提示:GC Roots:虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象;方法區(qū)中類靜態(tài)屬性引用的對(duì)象;方法區(qū)中常量引用的對(duì)象;本地方法棧中JNI(即一般說的native方法)中引用的對(duì)象
- 如果不是弱引用,而且用戶已經(jīng)不再持有這個(gè)ThreadLocal的引用并且沒有調(diào)用remove方法,那么只要線程還在,ThreadLocal和數(shù)據(jù)就會(huì)一直被引用無法回收,就是內(nèi)存泄漏了,所以這里用弱引用一定程度上是幫助忘記調(diào)用remove方法的用戶做清理工作…