使用 ThreadLocal 一次解决老大难问题
1.ThreadLocal的使用場景
1.1 場景1
每個線程需要一個獨享對象(通常是工具類,典型需要使用的類有SimpleDateFormat和Random)
每個Thread內有自己的實例副本,不共享
比喻:教材只有一本,一起做筆記有線程安全問題。復印后沒有問題,使用ThradLocal相當于復印了教材。
1.2 場景2
每個線程內需要保存全局變量(例如在攔截器中獲取用戶信息),可以讓不同方法直接使用,避免參數傳遞的麻煩
2.對以上場景的實踐
2.1 實踐場景1
/***?兩個線程打印日期*/ public?class?ThreadLocalNormalUsage00?{public?static?void?main(String[]?args)?throws?InterruptedException?{new?Thread(new?Runnable()?{@Overridepublic?void?run()?{String?date?=?new?ThreadLocalNormalUsage00().date(10);System.out.println(date);}}).start();new?Thread(new?Runnable()?{@Overridepublic?void?run()?{String?date?=?new?ThreadLocalNormalUsage00().date(104707);System.out.println(date);}}).start();}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");return?dateFormat.format(date);} }運行結果
因為中國位于東八區,所以時間從1970年1月1日的8點開始計算的
/***?三十個線程打印日期*/ public?class?ThreadLocalNormalUsage01?{public?static?void?main(String[]?args)?throws?InterruptedException?{for?(int?i?=?0;?i?<?30;?i++)?{int?finalI?=?i;new?Thread(new?Runnable()?{@Overridepublic?void?run()?{String?date?=?new?ThreadLocalNormalUsage01().date(finalI);System.out.println(date);}}).start();//線程啟動后,休眠100msThread.sleep(100);}}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");return?dateFormat.format(date);} }運行結果
多個線程打印自己的時間(如果線程超級多就會產生性能問題),所以要使用線程池。
/***?1000個線程打印日期,用線程池來執行*/ public?class?ThreadLocalNormalUsage02?{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()?{@Overridepublic?void?run()?{String?date?=?new?ThreadLocalNormalUsage02().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");return?dateFormat.format(date);} }運行結果
但是使用線程池時就會發現每個線程都有一個自己的SimpleDateFormat對象,沒有必要,所以將SimpleDateFormat聲明為靜態,保證只有一個
/***?1000個線程打印日期,用線程池來執行,出現線程安全問題*/ public?class?ThreadLocalNormalUsage03?{public?static?ExecutorService?threadPool?=?Executors.newFixedThreadPool(10);//只創建一次?SimpleDateFormat?對象,避免不必要的資源消耗static?SimpleDateFormat?dateFormat?=?new?SimpleDateFormat("yyyy-MM-dd?hh:mm:ss");public?static?void?main(String[]?args)?throws?InterruptedException?{for?(int?i?=?0;?i?<?1000;?i++)?{int?finalI?=?i;//提交任務threadPool.submit(new?Runnable()?{@Overridepublic?void?run()?{String?date?=?new?ThreadLocalNormalUsage03().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);return?dateFormat.format(date);} }運行結果
出現了秒數相同的打印結果,這顯然是不正確的。
出現問題的原因
多個線程的task指向了同一個SimpleDateFormat對象,SimpleDateFormat是非線程安全的。
解決問題的方案
方案1:加鎖
格式化代碼是在最后一句return dateFormat.format(date);,所以可以為最后一句代碼添加synchronized鎖
public?String?date(int?seconds)?{//參數的單位是毫秒,從1970.1.1?00:00:00?GMT?開始計時Date?date?=?new?Date(1000?*?seconds);String?s;synchronized?(ThreadLocalNormalUsage04.class)?{s?=?dateFormat.format(date);}return?s; }運行結果
運行結果中沒有發現相同的時間,達到了線程安全的目的
缺點:因為添加了synchronized,所以會保證同一時間只有一條線程可以執行,這在高并發場景下肯定不是一個好的選擇,所以看看其他方案吧。
方案2:使用ThreadLocal
/***?利用?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()?{@Overridepublic?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?對象SimpleDateFormat?dateFormat?=?ThreadSafeFormatter.dateFormatThreadLocal.get();return?dateFormat.format(date);} }class?ThreadSafeFormatter?{public?static?ThreadLocal<SimpleDateFormat>?dateFormatThreadLocal?=?newThreadLocal<SimpleDateFormat>(){//創建一份?SimpleDateFormat?對象@Overrideprotected?SimpleDateFormat?initialValue()?{return?new?SimpleDateFormat("yyyy-MM-dd?hh:mm:ss");}}; }運行結果
使用了ThreadLocal后不同的線程不會有共享的 SimpleDateFormat 對象,所以也就不會有線程安全問題
2.2 實踐場景2
當前用戶信息需要被線程內的所有方法共享
方案1:傳遞參數
可以將user作為參數在每個方法中進行傳遞,
缺點:但是這樣做會產生代碼冗余問題,并且可維護性差。
方案2:使用Map
對此進行改進的方案是使用一個Map,在第一個方法中存儲信息,后續需要使用直接get()即可,
缺點:如果在單線程環境下可以保證安全,但是在多線程環境下是不可以的。如果使用加鎖和ConcurrentHashMap都會產生性能問題。
方案3:使用ThreadLocal,實現不同方法間的資源共享
使用 ThreadLocal 可以避免加鎖產生的性能問題,也可以避免層層傳遞參數來實現業務需求,就可以實現不同線程中存儲不同信息的要求。
/***?演示 ThreadLocal 的用法2:避免參數傳遞的麻煩*/ public?class?ThreadLocalNormalUsage06?{public?static?void?main(String[]?args)?{new?Service1().process();} }class?Service1?{public?void?process()?{User?user?=?new?User("魯毅");//將User對象存儲到?holder?中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);} }class?UserContextHolder?{public?static?ThreadLocal<User>?holder?=?new?ThreadLocal<>(); }class?User?{String?name;public?User(String?name)?{this.name?=?name;} }運行結果
3. 對ThreadLocal的總結
-
讓某個需要用到的對象實現線程之間的隔離(每個線程都有自己獨立的對象)
-
可以在任何方法中輕松的獲取到該對象
-
根據共享對象生成的時機選擇使用initialValue方法還是set方法
-
對象初始化的時機由我們控制的時候使用initialValue 方式
-
如果對象生成的時機不由我們控制的時候使用 set 方式
4. 使用ThreadLocal的好處
-
達到線程安全的目的
-
不需要加鎖,執行效率高
-
更加節省內存,節省開銷
-
免去傳參的繁瑣,降低代碼耦合度
5. ThreadLocal原理
-
Thread
-
ThreadLocal
-
ThreadLocalMap
在Thread類內部有有ThreadLocal.ThreadLocalMap threadLocals = null;這個變量,它用于存儲ThreadLocal,因為在同一個線程當中可以有多個ThreadLocal,并且多次調用get()所以需要在內部維護一個ThreadLocalMap用來存儲多個ThreadLocal
5.1 ThreadLocal相關方法
T initialValue()
該方法用于設置初始值,并且在調用get()方法時才會被觸發,所以是懶加載。
但是如果在get()之前進行了set()操作,這樣就不會調用initialValue()。
通常每個線程只能調用一次本方法,但是調用了remove()后就能再次調用
public?T?get()?{Thread?t?=?Thread.currentThread();ThreadLocalMap?map?=?getMap(t);//獲取到了值直接返回resuleif?(map?!=?null)?{ThreadLocalMap.Entry?e?=?map.getEntry(this);if?(e?!=?null)?{@SuppressWarnings("unchecked")T?result?=?(T)e.value;return?result;}}//沒有獲取到才會進行初始化return?setInitialValue(); }private?T?setInitialValue()?{//獲取initialValue生成的值,并在后續操作中進行set,最后將值返回T?value?=?initialValue();Thread?t?=?Thread.currentThread();ThreadLocalMap?map?=?getMap(t);if?(map?!=?null)map.set(this,?value);elsecreateMap(t,?value);return?value; }public?void?remove()?{ThreadLocalMap?m?=?getMap(Thread.currentThread());if?(m?!=?null)m.remove(this); }void set(T t)
為這個線程設置一個新值
public?void?set(T?value)?{Thread?t?=?Thread.currentThread();ThreadLocalMap?map?=?getMap(t);if?(map?!=?null)map.set(this,?value);elsecreateMap(t,?value); }T get()
獲取線程對應的value
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(); }void remove()
刪除對應這個線程的值
6.ThreadLocal注意點
6.1 內存泄漏
內存泄露;某個對象不會再被使用,但是該對象的內存卻無法被收回
static?class?ThreadLocalMap?{static?class?Entry?extends?WeakReference<ThreadLocal<?>>?{/**?The?value?associated?with?this?ThreadLocal.?*/Object?value;Entry(ThreadLocal<?>?k,?Object?v)?{//調用父類,父類是一個弱引用super(k);//強引用value?=?v;}}強引用:當內存不足時觸發GC,寧愿拋出OOM也不會回收強引用的內存
弱引用:觸發GC后便會回收弱引用的內存
正常情況
當Thread運行結束后,ThreadLocal中的value會被回收,因為沒有任何強引用了
非正常情況
當Thread一直在運行始終不結束,強引用就不會被回收,存在以下調用鏈?Thread-->ThreadLocalMap-->Entry(key為null)-->value因為調用鏈中的 value 和 Thread 存在強引用,所以value無法被回收,就有可能出現OOM。
JDK的設計已經考慮到了這個問題,所以在set()、remove()、resize()方法中會掃描到key為null的Entry,并且把對應的value設置為null,這樣value對象就可以被回收。
private?void?resize()?{Entry[]?oldTab?=?table;int?oldLen?=?oldTab.length;int?newLen?=?oldLen?*?2;Entry[]?newTab?=?new?Entry[newLen];int?count?=?0;for?(int?j?=?0;?j?<?oldLen;?++j)?{Entry?e?=?oldTab[j];if?(e?!=?null)?{ThreadLocal<?>?k?=?e.get();//當ThreadLocal為空時,將ThreadLocal對應的value也設置為nullif?(k?==?null)?{e.value?=?null;?//?Help?the?GC}?else?{int?h?=?k.threadLocalHashCode?&?(newLen?-?1);while?(newTab[h]?!=?null)h?=?nextIndex(h,?newLen);newTab[h]?=?e;count++;}}}setThreshold(newLen);size?=?count;table?=?newTab; }但是只有在調用set()、remove()、resize()這些方法時才會進行這些操作,如果沒有調用這些方法并且線程不停止,那么調用鏈就會一直存在,所以可能會發生內存泄漏。
6.2 如何避免內存泄漏(阿里規約)
調用remove()方法,就會刪除對應的Entry對象,可以避免內存泄漏,所以使用完ThreadLocal后,要調用remove()方法。
class?Service1?{public?void?process()?{User?user?=?new?User("魯毅");//將User對象存儲到?holder?中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();} }6.3 ThreadLocal的空指針異常問題
/***?ThreadLocal的空指針異常問題*/ public?class?ThreadLocalNPE?{ThreadLocal<Long>?longThreadLocal?=?new?ThreadLocal<>();public?void?set()?{longThreadLocal.set(Thread.currentThread().getId());}public?Long?get()?{return?longThreadLocal.get();}public?static?void?main(String[]?args)?{ThreadLocalNPE?threadLocalNPE?=?new?ThreadLocalNPE();//如果get方法返回值為基本類型,則會報空指針異常,如果是包裝類型就不會出錯System.out.println(threadLocalNPE.get());Thread?thread1?=?new?Thread(new?Runnable()?{@Overridepublic?void?run()?{threadLocalNPE.set();System.out.println(threadLocalNPE.get());}});thread1.start();} }6.4 空指針異常問題的解決
如果get方法返回值為基本類型,則會報空指針異常,如果是包裝類型就不會出錯。這是因為基本類型和包裝類型存在裝箱和拆箱的關系,造成空指針問題的原因在于使用者。
6.5 共享對象問題
如果在每個線程中ThreadLocal.set()進去的東西本來就是多個線程共享的同一對象,比如static對象,那么多個線程調用ThreadLocal.get()獲取的內容還是同一個對象,還是會發生線程安全問題。
6.6 可以不使用ThreadLocal就不要強行使用
如果在任務數很少的時候,在局部方法中創建對象就可以解決問題,這樣就不需要使用ThreadLocal。
6.7 優先使用框架的支持,而不是自己創造
例如在Spring框架中,如果可以使用RequestContextHolder,那么就不需要自己維護ThreadLocal,因為自己可能會忘記調用remove()方法等,造成內存泄漏。
本文僅為自己學習時記下的筆記,參考自慕課:
https://coding.imooc.com/class/409.html
總結
以上是生活随笔為你收集整理的使用 ThreadLocal 一次解决老大难问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 打死都要记住!微服务架构的常用设计模式!
- 下一篇: 那些年,我们踩过的 Java 坑