Java ThreadLocal 類的使用

基於 Java - ThreadLocal 類的使用整理

  • ThreadLocal 表示線程的局部變量,當前線程可以通過 set/get 來對這個局部變量進行操作,其他線程不能對其進行訪問

    • ThreadLocal 支持泛型,也就是支持指定 value 類型,像是ThreadLocal<Date>就是指定 value 爲 Date 類型。

    • 每個線程會有一份私有的 ThreadLocalMap 變量(threadLocals),去儲存這個線程自己想存放的 ThreadLocal 變量們。

      public class Thread implements Runnable {
          //Thread 類裏的 threadLocals 存放此線程的專有的 ThreadLocalMap
          ThreadLocal.ThreadLocalMap threadLocals = null;
      }
      

      ThreadLocalMap 可以理解爲是一個 Map,Map 的 key 是指向某個 ThreadLocal 對象的引用,value 就是這個線程自己 set 的值。實際上,ThreadLocalMap 中保存有一個 Entry 集合,Entry 中用兩個域分別保存 key 和 value。

    • 對於一個線程來說,一個 ThreadLocal 只能關聯一個值,而一個線程可以關聯多個 ThreadLocal。

      下面有個例子,在 main 線程中的 ThreadLocalMap,就有兩個 key-value 的映射,分別是 userIdThreadLocal -> 100、userNameThreadLocal -> hello。

      public class Main {
          public static void main(String[] args){
              ThreadLocal<Integer> userIdThreadLocal = new ThreadLocal<>();
              ThreadLocal<String> userNameThreadLocal = new ThreadLocal<>();
      
              userIdThreadLocal.set(100);
              userNameThreadLocal.set("hello");
          }
      }
      
    • 當調用 ThreadLocal tltl.get()方法時,其實就是先去取得此線程的 ThreadLocalMap,然後再去查找這個 Map 中的 key 爲 的tl那個 Entry 的 value 值。

  • ThreadLocal 常用的方法

    • set(x): 設置此線程局部變量的想要放的值是多少[1]

    • get(): 取得此線程局部變量當初存放的值,如果沒有存放過則返回 null;

    • remove(): 刪除此線程局部變量的鍵值對,也就是如果先執行 remove 再執行 get,會返回 null。

  • ThreadLocal 可以用在 SimpleDateFormat,或是 SpringMVC 上

    • 因爲 SimpleDateFormat 不是線程安全的,雖然可以每次要使用的時候重新new一個,但是這樣做會很浪費資源,所以如果使用 ThreadLocal 在每個線程裏都存放一個此線程專用的 SimpleDateFormat,就可以避免一直new的資源浪費,同時又確保線程安全。

    • 因爲 SpringMVC 會對每個請求分配一個線程,可以在攔截器將此線程的用戶信息(ip、名字…)使用 ThreadLocal 儲存,這樣在後續要用到用戶信息的地方時,就可以去 ThreadLocal 中取得,而且因爲 ThreadLocal 可以隔離線程,因此每條請求對應的線程的用戶信息不會互相干擾。

  • ThreadLocal 可能造成的內存泄漏

    之所以 ThreadLocal 會發生內存泄漏,原因是因爲只要線程活着,這個線程的 ThreadLocalMap 就會一直活着,而當初透過 ThreadLocal set() 的值,也就會在 ThreadLocalMap 中一直存在,也就是該 ThreadLocal 和該 value 的內存地址始終都有這個 ThreadLocalMap 在引用着(被 ThreadLocalMap 中的 Entry 直接引用),導致 GC 無法回收它,所以纔會發生內存泄漏。

    爲了解決這個問題,Java 做了一個小優化,在 ThreadLocalMap 中使用弱引用來指向 ThreadLocal,如果一個 ThreadLocal 沒有外部強引用來引用它,只有這條 ThreadLocalMap 的弱引用來引用它時,那麼當系統 GC 時,這些 ThreadLocal 就會被回收(因爲是弱引用),如此一來,ThreadLocalMap 中就會出現 key 爲 null 的 Entry 們。

    public class ThreadLocal<T> {
        //根據線程,取得那個線程自己的 ThreadLocalMap
        ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }
    
        static class ThreadLocalMap {
            //ThreadLocalMap 的 key 是使用“弱引用”的 ThreadLocal
            static class Entry extends WeakReference<ThreadLocal> {
                Object value;
    
                //ThreadLocalMap 中的 key 就是 ThreadLocal,value 就是設置的值
                Entry(ThreadLocal k, Object v) {
                    super(k);
                    value = v;
                }
            }
        }
    }
    

    下圖中,實線表示強引用,虛線表示弱引用:

    img

    這個弱引用優化只能使得 ThreadLocal 被正確回收,但是這些 key 爲 null 的 Entry 們仍然會存在在 ThreadLocalMap 裏,因此 value 仍然無法被回收。

    所以 Java 又做了一個優化,就是在 ThreadLocal 執行get()set()remove()方法時,都會將該線程 ThreadLocalMap 裏所有 key = null 的 value 也設置爲 null,手動幫助 GC:

    ThreadLocal k = e.get();
    if (k == null) {
        e.value = null; // Help the GC
    } 
    

    但是根本上的解決辦法,還是在當前線程使用完這個 ThreadLocal 時,就及時remove()掉該 value,也就是使得 ThreadLocalMap 中不要存在這個鍵值對,這樣才能確保 GC 能正確回收。

  • 具體實例

    • 每個線程都可以在 ThreadLocal 中放自己的值,且不會干擾到其他線程的值

      class Tools {
          public static ThreadLocal threadLocal = new ThreadLocal();
      }
      
      class MyThread extends Thread {
          @Override
          public void run() {
              if (Tools.threadLocal.get() == null) {
                  Tools.threadLocal.set(Thread.currentThread().getName() + ", " + Math.random());
              }
              System.out.println(Tools.threadLocal.get());
          }
      }
      
      public class Main {
          public static void main(String[] args) {
              for (int i = 0; i < 5; i++) {
                  MyThread thread = new MyThread();
                  thread.setName("thread " + i);
                  thread.start();
              }
          }
      }
      
      thread 1, 0.86
      thread 0, 0.42
      thread 2, 0.35
      thread 3, 0.41
      thread 4, 0.45
      

      可以看到,不同線程的 ThreadLocalMap 中的 key 可能指向同一個 ThreadLocal 對象,也就是 ThreadLocal 可以是多線程共享的,但是 key 對應的 value 因爲保存在每個線程單獨的 ThreadLocalMap 中,而無法多線程共享。

      注意:這裏僅僅是爲了演示而使用 public static 修飾 ThreadLocal,這會產生一個指向 ThreadLocal 的強引用,可能導致內存泄漏。

    • 使用 ThreadLocal 在 SimpleDateFormat 上,並且給 ThreadLocal 加上泛型,指定 value 的類型是 SimpleDateFormat

      因爲使用了 ThreadLocal 確保每個線程有自己一份 SimpleDateFormat,所以線程安全,不會報錯。

      class MyThread extends Thread {
          private static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>();
          
          @Override
          public void run() {
              SimpleDateFormat sdf = threadLocal.get();
              if (sdf == null) {
                  sdf = new SimpleDateFormat("yyyy-MM-dd");
                  threadLocal.set(sdf);
              }
              try {
                  System.out.println(sdf.parse("2018-07-15"));
              } catch (ParseException e) {
                  System.out.println("報錯了");
              }
          }
      }
      
      public class Main {
          public static void main(String[] args) {
              for (int i = 0; i < 5; i++) {
                  MyThread thread = new MyThread();
                  thread.setName("thread " + i);
                  thread.start();
              }
          }
      }
      
      Sun Jul 15 00:00:00 CST 2018
      Sun Jul 15 00:00:00 CST 2018
      Sun Jul 15 00:00:00 CST 2018
      Sun Jul 15 00:00:00 CST 2018
      Sun Jul 15 00:00:00 CST 2018
      

      另:注意到示例中使用 private static 修飾 ThreadLocal,這通常是受鼓勵的做法,private 是爲了封裝,static 是爲了避免重複創建 TSO(Thread Specific Object,即與線程相關的變量)。沒有被 static 修飾的 ThreadLocal 變量實例,會隨着所在的類多次創建而被多次實例化,這樣頻繁地創建變量實例沒有必要。

    • 使用 ThreadLocal 在 SpringMVC 上

      • 攔截器 MyInterceptor 先去從 cookie 中取得當前用戶信息,透過 UserUtils 放到ThreadLocal<User>

      • 然後當 MyController 要去取得這個請求(也就是這條線程)的用戶信息時,就去調用 UserUtils 取得放在ThreadLocal<User>裏面的 User 信息

      • 最後當請求結束時,刪除此條線程的ThreadLocal<User>信息,避免內存泄漏

        //UserUtils 專門存取 User 信息
        public class UserUtils {
            public static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
        
            public static void setUser(User user) {
                userThreadLocal.set(user);
            }
        
            public static User getUser() {
                return userThreadLocal.get();
            }
        
            public static void removeUser() {
                if (userThreadLocal.get() != null) {
                    userThreadLocal.remove();
                }
            }
        }
        
        //攔截器取得 cookie 中的 User 信息,並調用 UserUtils 放到 ThreadLocal 裏
        //請求結束時要記得把 ThreadLocal 中的 User 刪除,因爲這條線程之後還要去服務其他請求
        public class MyInterceptor extends HandlerInterceptorAdapter {
            @Override
            public boolean preHandle() throws Exception {
                User user = getUserFromCookie();
                UserUtils.setUser(user);
                return true;
            }
        
            @Override
            public void postHandle() throws Exception {
                UserUtils.removeUser();
            }
        }
        
        //MyController 調用 UserUtils 取得 ThreadLocal<User> 中的 User
        @Controller
        @RequestMapping("/")
        public class MyController {
            @RequestMapping("/")
            public void test() {
                User user = UserUtils.getUser();
                System.out.println("User id: " + user.id);
            }
        }
        

參考:

ThreadLocal 的 Entry 爲什麼要繼承 WeakReference?

ThreadLocal 原理及使用場景

爲何通常“將 ThreadLocal 變量設置爲 static”?

將 ThreadLocal 變量設置爲 private static 的好處是啥?


  1. 注意:從 API 上看,似乎是將變量值保存在了 ThreadLocal 中,但是從源碼上看,變量值實際上保存在了 ThreadLocalMap 中或者說 Thread 中。下面看到類似於“在 ThreadLocal 中放值”這樣的說法時,應該意識到在源碼層面上的另一種解釋。 ↩︎

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章