Java之ThreadLocal使用常見和方式案例講解
【面試高頻】- ThreadLocal的使用場景以及使用方式是怎麼樣的
1 兩大使用場景-ThreadLocal的用途
- 典型場景1:每個線程需要一個獨享的對象(通常是工具類,典型需要使用的類有SimpleDateFormat和Random)
- 典型場景2:每個線程內需要保存全局變量(例如在攔截器中獲取用戶信息),可以讓不同方法直接使用,避免參數傳遞的麻煩。
2 典型場景1:每個線程需要一個獨享的對象
每個Thread內有自己的實例副本,不共享;
舉例:SimpleDateFormat。(當多個線程共用這樣一個SimpleDateFormat,但是這個類是不安全的)
- 2個線程分別用自己的SimpleDateFormat,這沒問題;
- 後來延伸出10個,那就有10個線程和10個SimpleDateFormat,這雖然寫法不優雅,但勉強可以接受
- 但是當需求變成瞭1000,那麼必然要用線程池,消耗內存太多;
- 但是每一個SimpleDateFormat我們都需要創建一遍,那麼太耗費new對象瞭,改成static共用的,所有線程都共用一個simpleDateFormat對象,但這是線程不安全的,容易出現時間一致的情況,在調用的時候,可加鎖來解決,但還是不優雅;
- 用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對象,保證瞭線程安全,高效利用內存 */ 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) { //參數的單位是毫秒,從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")); }
3 典型場景2:當前用戶信息需要被線程內所有方法共享
- 一個比較繁瑣的解決方案是把user作為參數層層傳遞,從service-1()傳到service-2(),以此類推,但是這樣做會導致代碼冗餘且不易維護。
- 進階點就是userMap來保存,但是當多線程同時工作時,需要保證線程安全,需要用synchronized,或者concurrenthashmap,但無論用什麼,都會對性能有所影響
每個線程內需要保存全局變量,可以讓不同方法直接使用,避免參數傳遞的麻煩
- 用ThreadLocal保存一些業務內存(用戶權限信息,從用戶系統獲取到的用戶名、userId等)
- 這些信息在同一個線程內相同,但是不同的線程使用的業務內容是不相同的
- 在線程生命周期內,都通過這個靜態ThreadLocal實例的get()方法取得自己set過的那個對象,避免瞭將這個對象作為參數傳遞的麻煩
package threadlocal; /** * 描述: 演示ThreadLocal用法2:避免傳遞參數的麻煩 */ 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; } }
註意點:
- 強調的是同一個請求內(同一個線程內)不同方法見的共享;
- 不需重寫initialValue()方法,但是必須手動調用set()方法
4 ThreadLocal方法使用總結
場景一:initialValue
在ThreadLocal第一次get的時候把對象給初始化出來,對象的初始化時機可以由我們控制。
場景二:set
如果需要保存到ThreadLocal裡面的對象的生成時機不由我們隨意控制。例如攔截器生成的用戶信息,用ThreadLocal.set直接放到ThreadLocal當中。
5 ThreadLocal原理
理清Thread,ThreadLocalMap以及ThreadLocal
主要方法介紹
- T initialValue(): 初始化
- void set(T t): 為這個線程設置一新值
- T get(): 得到這個線程對應的value。如果是首次調用get()。則會調用initialize來得到這個值
- void remove(): 刪除這個線程得到的值
ThreadLocalMap發生沖突之後,會用線性探測法。
6 ThreadLocal使用問題內存泄露
什麼是內存泄露:某個對象不再有用,但是占用的內存卻不能被回收。
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,會把value也設置為null,這樣value對象就可以被回收瞭。
- 但是如果一個ThreadLocal不被使用,那麼實際上set,remove,rehash方法也不會被調用,如果同時線程又不停止,那麼調用鏈就一直存在,那麼就導致瞭value的內存泄露。
如何避免內存泄露呢?
- 調用remove方法,就會刪除對應的Entry對象,可以避免內存泄露,所以使用完ThreadLocal之後,應該調用remove方法。
7 實際應用場景-在spring中的實例分析
- DateTimeContextHolder:用到瞭ThreadLocal
- RequestContextHolder:用到瞭ThreadLocal
到此這篇關於Java之ThreadLocal使用常見和方式案例講解的文章就介紹到這瞭,更多相關Java之ThreadLocal使用常見和方式內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- 徹底搞懂Java多線程(四)
- Java8的DateTimeFormatter與SimpleDateFormat的區別詳解
- java中ThreadLocal的應用場景實例分析
- Java ThreadLocal類使用詳解
- 詳解Java中ThreadLocal類型及簡單用法