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<SimpleDateFormat> 
               dateFormatThreadLocal 
                           = new ThreadLocal<SimpleDateFormat>() {
              @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<ThreadLocal<?>> {
              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<ThreadLocal<?>> {
          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);
    }
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355

推荐阅读更多精彩内容