• 听说你看过 ThreadLocal 源码?


      话不多说,直接进入今天的主题,本文的主要内容如下图所示:
      全文共10000+字,31张图,这篇文章同样耗费了不少的时间和精力才创作完成,请大家点点关注+点赞,感谢。
      源于壹枝花算不算浪漫 ,作者壹枝花算不算浪漫。https://www.cqxftyyj.com
      对于ThreadLocal,大家的第一反应可能是很简单呀,线程的变量副本,每个线程隔离。那这里有几个问题大家可以思考一下:
      ThreadLocal的key是弱引用,那么在 threadLocal.get()的时候,发生GC之后,key是否为null?
      ThreadLocal中ThreadLocalMap的数据结构?
      ThreadLocalMap的Hash算法?
      ThreadLocalMap中Hash冲突如何解决?
      ThreadLocalMap扩容机制?
      ThreadLocalMap中过期key的清理机制?探测式清理和启发式清理流程?
      ThreadLocalMap.set()方法实现原理?
      ThreadLocalMap.get()方法实现原理?
      项目中ThreadLocal使用情况?遇到的坑?
      ……
      上述的一些问题你是否都已经掌握的很清楚了呢?本文将围绕这些问题使用图文方式来剖析ThreadLocal的点点滴滴。
      全文目录
      ThreadLocal代码演示
      ThreadLocal的数据结构
      GC 之后key是否为null?
      ThreadLocal.set()方法源码详解
      ThreadLocalMap Hash算法
      ThreadLocalMap Hash冲突
      ThreadLocalMap.set()详解7.1 ThreadLocalMap.set()原理图解7.2 ThreadLocalMap.set()源码详解
      ThreadLocalMap过期key的探测式清理流程
      ThreadLocalMap扩容机制
      ThreadLocalMap.get()详解10.1 ThreadLocalMap.get()图解10.2 ThreadLocalMap.get()源码详解
      ThreadLocalMap过期key的启发式清理流程
      InheritableThreadLocal
      ThreadLocal项目中使用实战13.1 ThreadLocal使用场景13.2 分布式TraceId解决方案
      注明: 本文源码基于JDK 1.8
      ThreadLocal代码演示
      我们先看下ThreadLocal使用示例:
      public class ThreadLocalTest {
          private List<String> messages = Lists.newArrayList();
          public static final ThreadLocal<ThreadLocalTest> holder = ThreadLocal.withInitial(ThreadLocalTest::new);
          public static void add(String message) {
              holder.get().messages.add(message);
          }
          public static List<String> clear() {
              List<String> messages = holder.get().messages;
              holder.remove();
              System.out.println("size: " + holder.get().messages.size());
              return messages;
          }
          public static void main(String[] args) {
              ThreadLocalTest.add("一枝花算不算浪漫");
              System.out.println(holder.get().messages);
              ThreadLocalTest.clear();
          }
      }
      打印结果:
      [一枝花算不算浪漫]
      size: 0
      ThreadLocal对象可以提供线程局部变量,每个线程Thread拥有一份自己的副本变量,多个线程互不干扰。
      ThreadLocal的数据结构
      Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。
      ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。
      每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
      ThreadLocalMap有点类似HashMap的结构,只是HashMap是由数组+链表实现的,而ThreadLocalMap中并没有链表结构。
      我们还要注意Entry, 它的key是ThreadLocal<?> k ,继承自WeakReference, 也就是我们常说的弱引用类型。
      GC 之后key是否为null?
      回应开头的那个问题, ThreadLocal 的key是弱引用,那么在threadLocal.get()的时候,发生GC之后,key是否是null?
      为了搞清楚这个问题,我们需要搞清楚Java的四种引用类型:
      强引用:我们常常new出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候
      软引用:使用SoftReference修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收
      弱引用:使用WeakReference修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
      虚引用:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知
      接着再来看下代码,我们使用反射的方式来看看GC后ThreadLocal中的数据情况:
      public class ThreadLocalDemo {
          public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
              Thread t = new Thread(()->test("abc",false));
              t.start();
              t.join();
              System.out.println("--gc后--");
              Thread t2 = new Thread(() -> test("def", true));
              t2.start();
              t2.join();
          }
          private static void test(String s,boolean isGC)  {
              try {
                  new ThreadLocal<>().set(s);
                  if (isGC) {
                      System.gc();
                  }
                  Thread t = Thread.currentThread();
                  Class<? extends Thread> clz = t.getClass();
                  Field field = clz.getDeclaredField("threadLocals");
                  field.setAccessible(true);
                  Object threadLocalMap = field.get(t);
                  Class<?> tlmClass = threadLocalMap.getClass();
                  Field tableField = tlmClass.getDeclaredField("table");
                  tableField.setAccessible(true);
                  Object[] arr = (Object[]) tableField.get(threadLocalMap);
                  for (Object o : arr) {
                      if (o != null) {
                          Class<?> entryClass = o.getClass();
                          Field valueField = entryClass.getDeclaredField("value");
                          Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
                          valueField.setAccessible(true);
                          referenceField.setAccessible(true);
                          System.out.println(String.format("弱引用key:%s,值:%s", referenceField.get(o), valueField.get(o)));
                      }
                  }
              } catch (Exception e) {
                  e.printStackTrace();
              }
          }
      }
      结果如下:
      弱引用key:java.lang.ThreadLocal@433619b6,值:abc
      弱引用key:java.lang.ThreadLocal@418a15e3,值:java.lang.ref.SoftReference@bf97a12
      --gc后--
      弱引用key:null,值:def
      如图所示,因为这里创建的ThreadLocal并没有指向任何值,也就是没有任何引用:
      new ThreadLocal<>().set(s);
      所以这里在GC之后,key就会被回收,我们看到上面debug中的referent=null, 如果改动一下代码:
      这个问题刚开始看,如果没有过多思考,弱引用,还有垃圾回收,那么肯定会觉得是null。
      其实是不对的,因为题目说的是在做 threadlocal.get() 操作,证明其实还是有强引用存在的,所以 key 并不为 null,如下图所示,ThreadLocal的强引用仍然是存在的。
      如果我们的强引用不存在的话,那么 key 就会被回收,也就是会出现我们 value 没被回收,key 被回收,导致 value 永远存在,出现内存泄漏。
      ThreadLocal.set()方法源码详解
      ThreadLocal中的set方法原理如上图所示,很简单,主要是判断ThreadLocalMap是否存在,然后使用ThreadLocal中的set方法进行数据处理。
      代码如下:
      public void set(T value) {
          Thread t = Thread.currentThread();
          ThreadLocalMap map = getMap(t);
          if (map != null)
              map.set(this, value);
          else
              createMap(t, value);
      }
      void createMap(Thread t, T firstValue) {
          t.threadLocals = new ThreadLocalMap(this, firstValue);
      }
      主要的核心逻辑还是在ThreadLocalMap中的,一步步往下看,后面还有更详细的剖析。
      ThreadLocalMap Hash算法
      既然是Map结构,那么ThreadLocalMap当然也要实现自己的hash算法来解决散列表数组冲突问题。
      int i = key.threadLocalHashCode & (len-1);
      ThreadLocalMap中hash算法很简单,这里i就是当前key在散列表中对应的数组下标位置。
      这里最关键的就是threadLocalHashCode值的计算,ThreadLocal中有一个属性为HASH_INCREMENT = 0x61c88647
      public class ThreadLocal<T> {
          private final int threadLocalHashCode = nextHashCode();
          private static AtomicInteger nextHashCode = new AtomicInteger();
          private static final int HASH_INCREMENT = 0x61c88647;
          private static int nextHashCode() {
              return nextHashCode.getAndAdd(HASH_INCREMENT);
          }
          static class ThreadLocalMap {
              ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
                  table = new Entry[INITIAL_CAPACITY];
                  int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
                  table[i] = new Entry(firstKey, firstValue);
                  size = 1;
                  setThreshold(INITIAL_CAPACITY);
              }
          }
      }
      每当创建一个ThreadLocal对象,这个ThreadLocal.nextHashCode 这个值就会增长0x61c88647 。
      这个值很特殊,它是斐波那契数 也叫 黄金分割数。hash增量为 这个数字,带来的好处就是hash 分布非常均匀。
      我们自己可以尝试下:

  • 相关阅读:
    Hbase 性能改进
    HBase总结(十一)hbase Java API 介绍及使用示例
    Java中如何遍历Map对象的4种方法
    Jsp分页实例---假分页
    Jsp分页实例---真分页
    Java正则表达式
    平均时间复杂度为O(nlogn)的排序算法
    常见排序算法--简单排序
    [kuangbin带你飞]专题一 简单搜索
    [kuangbin带你飞]专题一 简单搜索
  • 原文地址:https://www.cnblogs.com/zqw111/p/12898943.html
Copyright © 2020-2023  润新知