Java - ThreadLocal 類的使用

古古

2018/07/16


  • ThreadLocal 是線程的局部變量, 是每一個線程所單獨持有的,其他線程不能對其進行訪問

    • ThreadLocal 支持泛型,也就是支持 value 是可以設置類型的,像是 ThreadLocal<Date> 就是設置 value 為 Date 類型
    • 每個線程會有自己的一份 ThreadLocalMap 變量,去儲存這個線程自己想存放的 ThreadLocal 變量們,他內部儲存的是一個鍵值對 Map,其中 key 是某個 ThreadLocal,value 就是這個線程自己 set 的值,所以對於一個線程來說,一個 ThreadLocal 只能存一個值,而一個線程可以存放好多個 ThreadLocal
    • 因此當調用 ThreadLocal tltl.get() 方法時,其實就是先去取得此線程的 ThreadLocalMap,然後再去查找這個 Map 中的 key 為 tl 的那個 Entry 的 value 值
  • ThreadLocal 常用的方法

    • set(x) : 設置此線程的想要放的值是多少
    • get() : 取得此線程當初存放的值,如果沒有存放過則返回 null
    • remove() : 刪除此線程的鍵值對,也就是如果先執行 remove 再執行 get,會返回 null
  • ThreadLocal 通常用在 SimpleDateFormat,或是 SpringMVC 上

    • 因為 SimpleDateFormat 不是線程安全的,因此雖然可以每次要使用的時候重新 new 一個,但是這樣做會很浪費資源,所以如果使用 ThreadLocal 在每個線程裡都存放一個此線程專用的 SimpleDateFormat,就可以避免一直 new 的資源浪費,又確保線程安全
    • 因為 SpringMVC 會對每個請求分配一個線程,可以在攔截器將此線程的用戶信息(ip、名字…)使用 ThreadLocal 儲存,這樣在後續要用到用戶信息的地方時,就可以去 ThreadLocal 中取得,而且因為 ThreadLocal 可以隔離線程,因此每條請求對應的線程的用戶信息不會互相干擾
  • ThreadLocal 可能造成的內存洩漏

    • 在Java裡,每個線程都有自己的 ThreadLocalMap,裡面存著這個線程自己私有的 ThreadLocal 們,而 ThreadLocalMap 的 key 為 ThreadLocal 實例,value 為私有對象 T,即是透過 set() 設置的值

      public class Thread implements Runnable {
          //Thread類裡的threadlocals存放此線程的專有的ThreadLocalMap
          ThreadLocal.ThreadLocalMap threadLocals = null;
      }
      
      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;
                  }
              }
          }
      }
      
    • 可以創建許多個 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> userNameThreacLocal = new ThreadLocal<>();
        
                userId.set(100);
                userName.set("hello");
            }
        }
        
    • 之所以 ThreadLocal 會發生內存洩漏,原因是因為只要線程活著,這個線程的 ThreadLocalMap 就會一直活著,而當初透過 ThreadLocal set() 的值,也就會在 ThreadLocalMap 中一直存在這個鍵值對不消失,所以該 ThreadLocal 和該 value 的內存地址始終都有這個 ThreadLocalMap 在引用著,導致 GC 無法回收他,所以才會發生內存洩漏

      • 為了解決這個問題,java 做了一個小優化,也就是存放在 ThreadLocalMap 中的 ThreadLocal,會使用 弱引用 來儲存,也就是說,如果一個 ThreadLocal 內存地址沒有外部強引用來引用他,只有這條 ThreadLocalMap 的弱引用來引用他時,那麼當系統 GC 時,這些 ThreadLocal 就會被回收(因為是弱引用),如此一來,ThreadLocalMap 中就會出現 key 為 null 的 Entry 們

        • 下圖中,實線表示強引用,虛線表示弱引用
      • 這個弱引用優化只能使得 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
      
    • 使用 ThreadLocal 在 SimpleDateFormat 上,並且給 ThreadLocal 加上泛型,指定 value 的類型是 SimpleDateFormat

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

        class Tools {
            public static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>();
        }
        
        class MyThread extends Thread {
            @Override
            public void run() {
                SimpleDateFormat sdf = Tools.threadLocal.get();
                if (sdf == null) {
                    sdf = new SimpleDateFormat("yyyy-MM-dd");
                    Tools.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
        
    • 使用 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();
            }
        }
        
        //MyContoller調用UserUtils取得ThreadLocal<User>中的User
        @Controller
        @RequestMapping("/")
        public class MyController {
            @RequestMapping("/")
            public void test() {
                User user = UserUtils.getUser();
                System.out.println("User id: " + user.id);
            }
        }