京東一面:說出ThreadLocal的使用場景及使用方式
點擊上方“碼農(nóng)突圍”,馬上關(guān)注
這里是碼農(nóng)充電第一站,回復(fù)“666”,獲取一份專屬大禮包 真愛,請設(shè)置“星標(biāo)”或點個“在看

兩大使用場景-ThreadLocal的用途
典型場景1: 每個線程需要一個獨享的對象(通常是工具類,典型需要使用的類有SimpleDateFormat和Random)
典型場景2: 每個線程內(nèi)需要保存全局變量(例如在攔截器中獲取用戶信息),可以讓不同方法直接使用,避免參數(shù)傳遞的麻煩。
典型場景1:每個線程需要一個獨享的對象
每個Thread內(nèi)有自己的實例副本,不共享;
舉例:SimpleDateFormat。(當(dāng)多個線程共用這樣一個SimpleDateFormat,但是這個類是不安全的)
2個線程分別用自己的SimpleDateFormat,這沒問題; 后來延伸出10個,那就有10個線程和10個SimpleDateFormat,這雖然寫法不優(yōu)雅,但勉強可以接受 但是當(dāng)需求變成了1000,那么必然要用線程池,消耗內(nèi)存太多; 但是每一個SimpleDateFormat我們都需要創(chuàng)建一遍,那么太耗費new對象了,改成static共用的,所有線程都共用一個simpleDateFormat對象,但這是線程不安全的,容易出現(xiàn)時間一致的情況,在調(diào)用的時候,可加鎖來解決,但還是不優(yōu)雅; 用ThreadLocal來解決該問題,給每個線程分配一個simpledateformat,可這個threadlocal是安全的;
package threadlocal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 描述: 利用ThreadLocal,給每個線程分配自己的dateFormat對象,保證了線程安全,高效利用內(nèi)存
*/
public class ThreadLocalNormalUsage05 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage05().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
//參數(shù)的單位是毫秒,從1970.1.1 00:00:00 GMT計時
Date date = new Date(1000 * seconds);
// SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
return dateFormat.format(date);
}
}
class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
典型場景2:當(dāng)前用戶信息需要被線程內(nèi)所有方法共享
一個比較繁瑣的解決方案是把user作為參數(shù)層層傳遞,從service-1()傳到service-2(),以此類推,但是這樣做會導(dǎo)致代碼冗余且不易維護(hù)。 進(jìn)階點就是userMap來保存,但是當(dāng)多線程同時工作時,需要保證線程安全,需要用synchronized,或者concurrenthashmap,但無論用什么,都會對性能有所影響
每個線程內(nèi)需要保存全局變量,可以讓不同方法直接使用,避免參數(shù)傳遞的麻煩
用ThreadLocal保存一些業(yè)務(wù)內(nèi)存(用戶權(quán)限信息,從用戶系統(tǒng)獲取到的用戶名、userId等) 這些信息在同一個線程內(nèi)相同,但是不同的線程使用的業(yè)務(wù)內(nèi)容是不相同的 在線程生命周期內(nèi),都通過這個靜態(tài)ThreadLocal實例的get()方法取得自己set過的那個對象,避免了將這個對象作為參數(shù)傳遞的麻煩
package threadlocal;
/**
* 描述: 演示ThreadLocal用法2:避免傳遞參數(shù)的麻煩
*/
public class ThreadLocalNormalUsage06 {
public static void main(String[] args) {
new Service1().process("");
}
}
class Service1 {
public void process(String name) {
User user = new User("超哥");
UserContextHolder.holder.set(user);
new Service2().process();
}
}
class Service2 {
public void process() {
User user = UserContextHolder.holder.get();
ThreadSafeFormatter.dateFormatThreadLocal.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();
}
}
class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name) {
this.name = name;
}
}
注意點:
強調(diào)的是同一個請求內(nèi)(同一個線程內(nèi))不同方法見的共享; 不需重寫initialValue()方法,但是必須手動調(diào)用set()方法
ThreadLocal方法使用總結(jié)
場景一:initialValue
在ThreadLocal第一次get的時候把對象給初始化出來,對象的初始化時機可以由我們控制。
場景二:set
如果需要保存到ThreadLocal里面的對象的生成時機不由我們隨意控制。例如攔截器生成的用戶信息,用ThreadLocal.set直接放到ThreadLocal當(dāng)中。
ThreadLocal原理
理清Thread,ThreadLocalMap以及ThreadLocal

主要方法介紹
T initialValue(): 初始化void set(T t): 為這個線程設(shè)置一新值T get(): 得到這個線程對應(yīng)的value。如果是首次調(diào)用get()。則會調(diào)用initialize來得到這個值void remove(): 刪除這個線程得到的值
ThreadLocalMap發(fā)生沖突之后,會用線性探測法。
ThreadLocal使用問題內(nèi)存泄露
什么是內(nèi)存泄露
某個對象不再有用,但是占用的內(nèi)存卻不能被回收。
Value的泄露
在ThreadLocalMap中的每個Entry都是一個對key的弱引用,同時,每個Entry都包含了一個對value的強引用。 正常情況 ,當(dāng)線程終止,保存在ThreadLocal里的value會被垃圾回收,因為沒有任何強引用了。 但是,如果線程不終止(比如線程池需要保持很久),那么key對應(yīng)的value就不能被回收。Thread->ThreadLocalMap->Entry(key為Null)->Value 因為value和Thread之間還存在這個強引用鏈路,所以導(dǎo)致value無法回收,就可能出現(xiàn)OOM;JDK已經(jīng)考慮到這個問題,所以在set,remove,rehash方法中會掃描key為null,會把value也設(shè)置為null,這樣value對象就可以被回收了。 但是如果一個ThreadLocal不被使用,那么實際上set,remove,rehash方法也不會被調(diào)用,如果同時線程又不停止,那么調(diào)用鏈就一直存在,那么就導(dǎo)致了value的內(nèi)存泄露。
如何避免內(nèi)存泄露呢?
調(diào)用remove方法,就會刪除對應(yīng)的Entry對象,可以避免內(nèi)存泄露,所以使用完ThreadLocal之后,應(yīng)該調(diào)用remove方法。
實際應(yīng)用場景-在spring中的實例分析
DateTimeContextHolder:用到了ThreadLocal RequestContextHolder:用到了ThreadLocal
- END - 最近熱文
? 身家破億!86版「紅孩兒」拒絕出道成學(xué)霸,已是中科院博士,名下52家公司 ? 96年出生,他已經(jīng)發(fā)了 8 篇 Nature論文! ? 靈隱寺招聘:沒有KPI,佛系上班…… ? 工程師在工作電腦存 64G 不雅文件,被公司開除后索賠 41 萬,結(jié)果…
評論
圖片
表情
