在通常的業務開發中,ThreadLocal 有兩種典型的使用場景
場景1:
ThreadLocal 用作保存每個線程獨享的對象,為每個線程都創建一個副本,這樣每個線程都可以修改自己所擁有的副本, 而不會影響其他線程的副本,確保了線程安全。
場景2:
ThreadLocal 用作每個線程內需要獨立保存信息,以便供其他方法更方便地獲取該信息的場景。每個線程獲取到的信息可能都是不一樣的,前面執行的方法保存了信息后,后續方法可以通過ThreadLocal 直接獲取到,避免了傳參,類似於全局變量的概念。
典型場景1
這種場景通常用於保存線程不安全的工具類,典型的需要使用的類就是 SimpleDateFormat。
在這種情況下,每個Thread內都有自己的實例副本,且該副本只能由當前Thread訪問到並使用,相當於每個線程內部的本地變量,這也是ThreadLocal命名的含義。因為每個線程獨享副本,而不是公用的,所以不存在多線程間共享的問題。
比如有10個線程都要用到SimpleDateFormat
public class ThreadLocalDemo01 { public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 10; i++) { int finalI = i; new Thread(() -> { String data = new ThreadLocalDemo01().date(finalI); System.out.println(data); }).start(); Thread.sleep(100); } } private String date(int seconds){ Date date = new Date(1000 * seconds); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss"); return simpleDateFormat.format(date); } }
我們給每個線程都創建了SimpleDateFormat對象,他們之間互不影響,代碼是可以正常執行的。輸出結果:
00:00 00:01 00:02 00:03 00:04 00:05 00:06 00:07 00:08 00:09
我們用圖來看一下當前的這種狀態:
如果有1000個線程都用到SimpleDateFormat對象呢?
我們一般不會直接去創建這么多線程,而是通過線程池,比如:
public class ThreadLocalDemo011 { public static ExecutorService threadPool = Executors.newFixedThreadPool(16); public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 1000; i++) { int finalI = i; threadPool.submit(() -> { String data = new ThreadLocalDemo011().date(finalI); System.out.println(data); }); } threadPool.shutdown(); } private String date(int seconds){ Date date = new Date(1000 * seconds); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss"); return simpleDateFormat.format(date); } }
可以看出,我們用了一個16線程的線程池,並且給這個線程池提交了1000次任務。每個任務中它做的事情和之前是一樣的,還是去執行date方法,並且在這個方法中創建一個
simpleDateFormat 對象。結果:
00:00 00:07 00:04 00:02 ... 16:29 16:28 16:27 16:26 16:39
我們剛才所做的就是每個任務都創建了一個 simpleDateFormat 對象,也就是說,1000 個任務對應 1000 個 simpleDateFormat 對象,但是如果任務數巨多怎么辦?
這么多對象的創建是有開銷的,並且在使用完之后的銷毀同樣是有開銷的,同時存在在內存中也是一種內存的浪費。
我們可能會想到,要不所有的線程共用一個 simpleDateFormat 對象?但是simpleDateFormat 又不是線程安全的,我們必須做同步,比如使用synchronized加鎖。到這里也許就是我們最終的一個解決方法。但是使用synchronized加鎖會陷入一種排隊的狀態,多個線程不能同時工作,這樣一來,整體的效率就被大大降低了。有沒有更好的解決方案呢?
使用ThreadLocal
對這種場景,ThreadLocal再合適不過了,ThreadLocal給每個線程維護一個自己的simpleDateFormat對象,這個對象在線程之間是獨立的,互相沒有關系的。這也就避免了線程安全問題。與此同時,simpleDateFormat對象還不會創造過多,線程池一共只有 16 個線程,所以需要16個對象即可。
public class ThreadLocalDemo04 { public static ExecutorService threadPool = Executors.newFixedThreadPool(16); public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 1000; i++) { int finalI = i; threadPool.submit(() -> { String data = new ThreadLocalDemo04().date(finalI); System.out.println(data); }); } threadPool.shutdown(); } private String date(int seconds){ Date date = new Date(1000 * seconds); SimpleDateFormat dateFormat = ThreadSafeFormater.dateFormatThreadLocal.get(); return dateFormat.format(date); } } class ThreadSafeFormater{ public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss")); }
我們用圖來看一下當前的這種狀態:
典型場景2
每個線程內需要保存類似於全局變量的信息(例如在攔截器中獲取的用戶信息),可以讓不同方法直接使用,避免參數傳遞的麻煩卻不想被多線程共享(因為不同線程獲取到的用戶信息不一樣)。
例如,用 ThreadLocal 保存一些業務內容(用戶權限信息、從用戶系統獲取到的用戶名、用戶ID 等),這些信息在同一個線程內相同,但是不同的線程使用的業務內容是不相同的。
在線程生命周期內,都通過這個靜態 ThreadLocal 實例的 get() 方法取得自己 set 過的那個對象,避免了將這個對象(如 user 對象)作為參數傳遞的麻煩。
比如說我們是一個用戶系統,那么當一個請求進來的時候,一個線程會負責執行這個請求,然后這個請求就會依次調用service-1()、service-2()、service-3()、service-4(),這4個方法可能是分布在不同的類中的。
我們用圖畫的形式舉一個實例:
代碼:
package com.kong.threadlocal; public class ThreadLocalDemo05 { public static void main(String[] args) { User user = new User("jack"); new Service1().service1(user); } } class Service1 { public void service1(User user){ //給ThreadLocal賦值,后續的服務直接通過ThreadLocal獲取就行了。 UserContextHolder.holder.set(user); new Service2().service2(); } } class Service2 { public void service2(){ User user = UserContextHolder.holder.get(); System.out.println("service2拿到的用戶:"+user.name); new Service3().service3(); } } class Service3 { public void service3(){ User user = UserContextHolder.holder.get(); System.out.println("service3拿到的用戶:"+user.name); //在整個流程執行完畢后,一定要執行remove UserContextHolder.holder.remove(); } } class UserContextHolder { //創建ThreadLocal保存User對象 public static ThreadLocal<User> holder = new ThreadLocal<>(); } class User { String name; public User(String name){ this.name = name; } }
執行的結果:
service2拿到的用戶:jack
service3拿到的用戶:jack