ThreadLocal

一、ThreadLocal 适合用在哪些实际生产的场景中

  • 保存每个线程独享的对象
    1. 为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。
    2. 这种场景通常用于保存线程不安全的工具类,典型的需要使用的类就是 SimpleDateFormat。
  • 案例
    1. 假设有个需求,即 2 个线程都要用到 SimpleDateFormat。代码如下所示:
      /** 
         有两个线程,那么就有两个 SimpleDateFormat 对象
         它们之间互不干扰,这段代码是可以正常运转的
         运行结果是:
         00:01
         00:02
       */
      public class ThreadLocalDemo01 {
          public static void main(String[] args) 
                                             throws InterruptedException {
              new Thread(() -> {
                  String date = new ThreadLocalDemo01().date(1);
                  System.out.println(date);
              }).start();
      
              Thread.sleep(100);
      
              new Thread(() -> {
                  String date = new ThreadLocalDemo01().date(2);
                  System.out.println(date);
              }).start();
          }
      
          public String date(int seconds) {
              Date date = new Date(1000 * seconds);
              SimpleDateFormat simpleDateFormat 
                                    = new SimpleDateFormat("mm:ss");
              return simpleDateFormat.format(date);
          }
      }
      
    2. 假设我们的需求有了升级,不仅仅需要 2 个线程,而是需要 10 个,也就是说,有 10 个线程同时对应 10 个 SimpleDateFormat 对象。我们就来看下面这种写法:
      /**
         利用了一个 for 循环来完成这个需求。
        for 循环一共循环 10 次,每一次都会新建一个线程
        每一个线程都会在 date 方法中创建一个 SimpleDateFormat 对象
        可以看出一共有 10 个线程,对应 10 个 SimpleDateFormat 对象。
        代码的运行结果:
        00:00
        00:01
        00:02
        00:03
        00:04
        00:05
        00:06
        00:07
        00:08
        00:09
        */
      public class ThreadLocalDemo02 {
          public static void main(String[] args) 
                                     throws InterruptedException {
              for (int i = 0; i < 10; i++) {
                  int finalI = i;
                  new Thread(() -> {
                      String date = new ThreadLocalDemo02().date(finalI);
                      System.out.println(date);
                  }).start();
                  Thread.sleep(100);
              }
          }
      
          public String date(int seconds) {
              Date date = new Date(1000 * seconds);
              SimpleDateFormat simpleDateFormat 
                                 = new SimpleDateFormat("mm:ss");
              return simpleDateFormat.format(date);
          }
      }
      
    3. 需求变成了 1000 个线程都要用到 SimpleDateFormat
      但是线程不能无休地创建下去,因为线程越多,所占用的资源也会越多。假设我们需要 1000 个任务,那就不能再用 for 循环的方法了,而是应该使用线程池来实现线程的复用,否则会消耗过多的内存等资源。
      public class ThreadLocalDemo06 {
      
          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(new Runnable() {
                      @Override
                      public void run() {
                          String date 
                                   = new ThreadLocalDemo06().date(finalI);
                          System.out.println(date);
                      }
                  });
              }
              threadPool.shutdown();
          }
      
          public String date(int seconds) {
              Date date = new Date(1000 * seconds);
              SimpleDateFormat dateFormat 
                    = ThreadSafeFormatter.dateFormatThreadLocal.get();
              return dateFormat.format(date);
          }
      }
      
      class ThreadSafeFormatter {
          public static ThreadLocal 
               dateFormatThreadLocal 
                           = new ThreadLocal() {
              @Override
              protected SimpleDateFormat initialValue() {
                  return new SimpleDateFormat("mm:ss");
              }
          };
      }
      
  • 保存一些业务内容
    每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息,日志信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。
    在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦。

二、ThreadLocal 是用来解决共享资源的多线程访问的问题吗

  1. 不是,ThreadLocal 并不是用来解决共享资源问题的。虽然 ThreadLocal 确实可以用于解决多线程情况下的线程安全问题,但其资源并不是共享的,而是每个线程独享的。
  2. ThreadLocal 解决线程安全问题的时候,相比于使用“锁”而言,换了一个思路,把资源变成了各线程独享的资源,非常巧妙地避免了同步操作。具体而言,它可以在 initialValue 中 new 出自己线程独享的资源,而多个线程之间,它们所访问的对象本身是不共享的,自然就不存在任何并发问题。这是 ThreadLocal 解决并发问题的最主要思路。

三、多个 ThreadLocal 在 Thread 中的 threadlocals 里是怎么存储的

  • Thread、 ThreadLocal 及 ThreadLocalMap 三者之间的关系
    1. 每个 Thread 对象中都持有一个 ThreadLocalMap 类型的成员变量
    2. 这个 ThreadLocalMap 自身类似于是一个 Map,里面会有一个个 key value 形式的键值对。
    3. key 就是 ThreadLocal 的引用
    4. value 这就是我们希望 ThreadLocal 存储的内容,例如 user 对象等
    5. 重点看到它们的数量对应关系:一个 Thread 里面只有一个ThreadLocalMap ,而在一个 ThreadLocalMap 里面却可以有很多的 ThreadLocal,每一个 ThreadLocal 都对应一个 value。因为一个 Thread 是可以调用多个 ThreadLocal 的,所以 Thread 内部就采用了 ThreadLocalMap 这样 Map 的数据结构来存放 ThreadLocal 和 value。


  • 源码分析
    1. get 方法
      public T get() {
          //获取到当前线程
          Thread t = Thread.currentThread();
          //获取到当前线程内的 ThreadLocalMap 对象,每个线程内都有一个 ThreadLocalMap 对象
          ThreadLocalMap map = getMap(t);
          if (map != null) {
              //获取 ThreadLocalMap 中的 Entry 对象并拿到 Value
              ThreadLocalMap.Entry e = map.getEntry(this);
              if (e != null) {
                  @SuppressWarnings("unchecked")
                  T result = (T)e.value;
                  return result;
              }
          }
          //如果线程内之前没创建过 ThreadLocalMap,就创建
          return setInitialValue();
      }
      
    2. getMap 方法
      这个方法很清楚地表明了 Thread 和 ThreadLocalMap 的关系,可以看出 ThreadLocalMap 是线程的一个成员变量。这个方法的作用就是获取到当前线程内的 ThreadLocalMap 对象,每个线程都有 ThreadLocalMap 对象,而这个对象的名字就叫作 threadLocals,初始值为 null ThreadLocal.ThreadLocalMap threadLocals = null;
      ThreadLocalMap getMap(Thread t) {
          return t.threadLocals;
      }
      
    3. set 方法
      • 首先,它还是需要获取到当前线程的引用,并且利用这个引用来获取到 ThreadLocalMap
      • 如果 map == null 则去创建这个 map
      • 而当 map != null 的时候就利用 map.set 方法,把 value 给 set 进去
      • map.set(this, value) 传入的这两个参数中,第一个参数是 this,就是当前 ThreadLocal 的引用,这也再次体现了,在 ThreadLocalMap 中,它的 key 的类型是 ThreadLocal;而第二个参数就是我们所传入的 value,这样一来就可以把这个键值对保存到 ThreadLocalMap 中去了
      public void set(T value) {
          Thread t = Thread.currentThread();
          ThreadLocalMap map = getMap(t);
          if (map != null)
              map.set(this, value);
          else
              createMap(t, value);
      }
      
    4. ThreadLocalMap 类,也就是 Thread.threadLocals
      • ThreadLocalMap 类是每个线程 Thread 类里面的一个成员变量
      • 其中最重要的就是截取出的这段代码中的 Entry 内部类。
      • 在 ThreadLocalMap 中会有一个 Entry 类型的数组,名字叫 table。
      • 我们可以把 Entry 理解为一个 map,其键值对为:
        1. 键:当前的 ThreadLocal
        2. 值:实际需要存储的变量,比如 user 用户对象或者 simpleDateFormat 对象等。
      • ThreadLocalMap 既然类似于 Map,所以就和 HashMap 一样,也会有包括 set、get、rehash、resize 等一系列标准操作。
      • 但是,虽然思路和 HashMap 是类似的,但是具体实现会有一些不同:
        1. HashMap 在面对 hash 冲突的时候,采用的是拉链法。它会先把对象 hash 到一个对应的格子中,如果有冲突就用链表的形式往下链
        2. ThreadLocalMap 解决 hash 冲突的方式是不一样的,它采用的是线性探测法。如果发生冲突,并不会用链表的形式往下链,而是会继续寻找下一个空的格子
      static class ThreadLocalMap {
          static class Entry extends WeakReference> {
              Object value;
              Entry(ThreadLocal k, Object v) {
                  super(k);
                  value = v;
              }
          }
         private Entry[] table;
      //...
      }
      

四、内存泄漏——为何每次用完 ThreadLocal 都要调用 remove()

  • 什么是内存泄漏
    内存泄漏指的是,当某一个对象不再有用的时候,占用的内存却不能被回收,这就叫作内存泄漏。
  • Key 的泄漏
    1. 线程在访问了 ThreadLocal 之后,都会在它的 ThreadLocalMap 里面的 Entry 中去维护该 ThreadLocal 变量与具体实例的映射。
    2. 我们可能会在业务代码中执行了 ThreadLocal instance = null 操作,想清理掉这个 ThreadLocal 实例,但是假设我们在 ThreadLocalMap 的 Entry 中强引用了 ThreadLocal 实例,那么,虽然在业务代码中把 ThreadLocal 实例置为了 null,但是在 Thread 类中依然有这个引用链的存在。
    3. GC 在垃圾回收的时候会进行可达性分析,它会发现这个 ThreadLocal 对象依然是可达的,所以对于这个 ThreadLocal 对象不会进行垃圾回收,这样的话就造成了内存泄漏的情况。
    4. JDK 开发者考虑到了这一点,所以 ThreadLocalMap 中的 Entry 继承了 WeakReference 弱引用,代码如下所示:
      static class Entry extends WeakReference> {
          Object value;
          Entry(ThreadLocal k, Object v) {
              super(k);
              value = v;
          }
      }
      
  • Value 的泄漏
    1. 仔细看上面Entry代码,发现虽然 ThreadLocalMap 的每个 Entry 都是一个对 key 的弱引用,但是这个 Entry 包含了一个对 value 的强引用
    2. 如果线程执行耗时任务而不停止,那么当垃圾回收进行可达性分析的时候,这个 Value 就是可达的,所以不会被回收
    3. 但是与此同时可能我们已经完成了业务逻辑处理,不再需要这个 Value 了,此时也就发生了内存泄漏问题
  • 如何避免内存泄露
    调用 ThreadLocal 的 remove 方法
    public void remove() {
        ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null)
            m.remove(this);
    }
    

你可能感兴趣的:(ThreadLocal)