若有不對之處歡迎大家指出,這個也是在學習工作中的一些總結,侵刪!
得之在俄頃,積之在平日。
1、使用場景
每個線程需要獨享的對象(通常是工具類,典型需要使用的類有SimpleDateFormart和Random),每個Thread內有自己的實例副本,不共享。
每個線程內需要保存全局變量(例如在攔截器中獲取用戶信息),可以在不同的地方直接使用,避免參數傳遞的麻煩,例如:當前用戶信息需要被線程內的所有方法共享,一個比較繁瑣的解決方案是把user作爲參數層層傳遞,從一個service傳遞到另一個service,以此類推,但是這樣會導致代碼冗餘且不易維護;解決方法:如果用ThreadLocal保存一些業務內容(用戶權限、信息,從用戶系統獲取到的用戶名、UserID等),這些信息在同一個線程內相同,但是不同的線程使用的業務內容是不同的,在線程的生命週期內,都通過這個靜態的ThreaLocal實例的get()方法取得自己set過的那個對象,避免了將這個對象(例如:User對象)作爲參數傳遞的麻煩。強調的是同一個請求內(同一個線程內)不同方法的共享。
2、ThreadLocal的兩個作用:
讓某個需要用到的對象在線程間隔離(每個線程都有自己獨立的對象)
在任何方法中都可以輕鬆的獲取到該對象
3、示例
一
注:採用java8新日期不會出現線程安全問題
//打印1000個日期 用10個線程來執行
//創建線程池
public static ExecutorService executorService = Executors.newFixedThreadPool(10);
//工具類,進行日期轉換
public static String DateTransition(long seconds) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-DD HH:mm:ss");
Date date = new Date(1000*seconds);
return dateFormat.format(date);
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
//i的十倍作爲毫秒數
long scond = i*10;
executorService.submit(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
String date = new ThreadLocalTest01().DateTransition(scond);
System.out.println("--->"+date);
}
});
}
executorService.shutdown();
}
這樣做看似沒什麼問題,實際上當執行1000次的時候則會創建1000個SimpleDateFormat,如下圖:
然後進行再次升級,將SimpleDateFormat提取出來作爲靜態常量,所有線程共享這一個SimpleDateFormat,就會發生線程安全問題
static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-DD HH:mm:ss");
//工具類,進行日期轉換
public String DateTransition(long seconds) {
Date date = new Date(1000*seconds);
return dateFormat.format(date);
}
在這裏我們可以選擇加鎖來解決線程安全問題。
//工具類,進行日期轉換
public String DateTransition(long seconds) {
Date date = new Date(1000*seconds);
String endDate = null;
synchronized (ThreadLocalTest01.class){
endDate = dateFormat.format(date);
}
return endDate;
}
加鎖能解決問題,但是會有性能開銷,如果使用ThreadLocal就不會存在這個問題
//打印1000個日期 用10個線程來執行
//創建線程池
public static ExecutorService executorService = Executors.newFixedThreadPool(10);
//static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-DD HH:mm:ss");
static ThreadLocal<SimpleDateFormat> threadLocaldateFormat = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-DD HH:mm:ss");
}
};
//工具類,進行日期轉換
public String DateTransition(long seconds) {
Date date = new Date(1000*seconds);
SimpleDateFormat simpleDateFormat = threadLocaldateFormat.get();
return simpleDateFormat.format(date);
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
//i的十倍作爲毫秒數
long scond = i*10;
executorService.submit(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
String date = new ThreadLocalTest01().DateTransition(scond);
System.out.println("--->"+date);
}
});
}
executorService.shutdown();
}
二
一個請求,調用多個業務,這樣就會將這個請求所帶的數據進行一層一層的傳遞,會導致代碼冗餘且不易維護
解決方案:採用ThreadLocal將參數放在一個map裏面,然後各層都能取到map裏面的數據,這樣每個線程裏面的map都不一樣
public class ThreadLocalTest02 {//一個請求,調用多個業務
public static void main(String[] args) {
UserRequest UserRequest = new UserRequest();
UserRequest.request("123");
}
}
class CreateThreadLocal{
public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
}
class UserRequest{
void request(String id){//請求
CreateThreadLocal.threadLocal.set(id);
UserService01 userService01 = new UserService01();
userService01.Service01();
}
}
class UserService01{
void Service01(){//業務一
System.out.println("Service01拿到數據:"+CreateThreadLocal.threadLocal.get());
UserService02 userService02 = new UserService02();
userService02.Service02();
}
}
class UserService02{
void Service02(){//業務二
System.out.println("Service02拿到數據:"+CreateThreadLocal.threadLocal.get());
UserService03 userService03 = new UserService03();
userService03.Service03();
}
}
class UserService03{
void Service03(){//業務三
System.out.println("Service03拿到數據:"+CreateThreadLocal.threadLocal.get());
CreateThreadLocal.threadLocal.remove();
}
}
4、initialValue使用場景:
在ThreadLocal第一次get的時候把對象初始化出來,對象的初始化時機可以由我們控制(常用於工具類)
5、set的使用場景
如果需要保存到ThreadLocal裏面的對象的生成時機不由我們隨意控制,例如攔截器生成的用戶信息,用ThreadLocal.set直接放到我們的ThreadLocal中去,以便後續使用。
6、使用ThreadLocal帶來的好處
達到線程安全。
不需要加鎖,提高執行效率。
更高的利用內存,節省開銷:相比於每個任務都新建一個SimpleDateFormat,顯然用ThreadLocal可以節省內存和開銷。
免去傳參的繁瑣,ThreadLocal使得代碼耦合度低,更優雅。
7、解析
ThreadLocalMap類:
也就是ThreadLocals, ThreadLocalMap類是每個線程Thread裏面的變量,裏面最重要的是一個鍵值對數組Entry[] table,可以認爲是一個map,鍵值對:
鍵:這個ThreadLocal
值:實際需要的成員變量,比如我們放進去的id
ThreadLocalMap採用的是線性探測法,也就是如果發生衝突,就繼續找下一個位置,而不是使用鏈表拉鍊,不想map那樣採用鏈表和紅黑樹
8、主要方法介紹:
T initialValue():初始化 。
【該方法會返回當前線程對應的“初始值”,這是一個延遲加載的方法,只有在調用get的時候纔會觸發,當線程第一次使用get方法訪問變量時調用此方法,除非線程先前調用了set方法,這種情況下,不會爲線程調用本initialValue方法;通常,每個線程最多調用一次此方法,但如果調用了remove()後再調用get(),則可再次調用此方法;如果不重寫本方法,這個方法會返回null,一般使用匿名內部類的方法來重寫initialValue()方法,以便在後續使用過程中可以初始化副本對象。】
void set(T t):爲這個線程設置新值。
T get():得到這個線程對應的value,如果是首次調用get,則會調用initialize來得到這個值。
void remove():刪除對應這個線程的值。
9、ThreadLocal注意點:
內存泄漏
內存泄漏是指某個對象不再有用,但是佔有的內存卻不能被回收
key的泄漏:ThreadLocalMap中的Entry繼承自 weakReference,是弱引用(弱應用的特點是,如果這個對象只被弱應用關 聯,沒有任何強引用關聯,那麼這個對象就可以被回收,所以弱引用不會 阻止GC)
![在這裏插入圖片描述](https://img-blog.csdnimg.cn/20200223135951481.png)
Value泄漏:ThreadLocalMap的每個Entry都是一個對key的 弱引用,同時每個Entry都包含了一個對Value的強引用,正常情況下, 當線程終止,保存在ThreadLocal裏面的Value會被垃圾回收,因爲沒有 任何強引用了,但是如果線程不終止或者持續很長時間,那麼key對應的 Value就不能被回收,因爲有以下調用鏈:
Thread->ThreadLocalMap->Entry(key爲null)->value
因爲Value和Thread之間還存在這個強引用鏈路,所以會導致Value無法 被回收,就可能出現OOM。JDK已經考慮到了這個問題,所以在 set,remove,rehash方法中會掃描key爲null的Entry,並把對應的value 值設置爲null,這樣對象就可以被回收,但是如果不調用這些方法,那麼 就會導致了Value的內存泄漏。
如何避免內存泄漏
調用remove()方法就會刪除對應的Entry對象,可以避免內存泄漏,所以在使用完ThreadLocal之後應該調用remove()方法
空指針異常
基本數據類型《==》包裝類
共享對象
不能將static放入ThreadLocal中
優先使用框架支持
例如:在Spring中,如果可以使用RequestContextHolder,那麼就不需要自己維護ThreadLocal,因爲自己可能會忘記調用remove等方法造成內存泄漏。