ThreadLocal源码阅读(JDK1.8)

一、简介

ThreadLocal 提供了线程间数据隔离的功能,从它的命名上也能知道这是属于一个线程的本地变量。也就是说,每个线程都会在 ThreadLocal 中保存一份该线程独有的数据,所以它是线程安全的。ThreadLocal的作用域就是线程。

一个简单的例子展示ThreadLocal的特性

public class ThreadLocalTest {
    public static void main(String[] args) {
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        // 创建一个有3个核心线程数的线程池
        ExecutorService threadPool = new ThreadPoolExecutor(3, 3, 1, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(10));
        // 线程池提交一个任务,将任务序号及执行该任务的子线程的线程名放到 ThreadLocal 中
        threadPool.execute(() -> threadLocal.set("任务1: " + Thread.currentThread().getName()));
        threadPool.execute(() -> threadLocal.set("任务2: " + Thread.currentThread().getName()));
        threadPool.execute(() -> threadLocal.set("任务3: " + Thread.currentThread().getName()));
        // 输出 ThreadLocal 中的内容
        for (int i = 0; i < 10; i++) {
            threadPool.execute(() -> System.out.println("ThreadLocal value of " + Thread.currentThread().getName() + " = " + threadLocal.get()));
        }
        // 线程池记得关闭
        threadPool.shutdown();
    }
}

运行结果如下:

image-20210421150838098

由此可见,线程池中执行任务的线程,随后多次输出线程在 ThreadLocal变量中存储的的内容也表明:每个线程在 ThreadLocal 中存储的内容是当前线程独有的,在多线程环境下,能够有效防止自己的变量被其他线程修改(存储的内容是同一个引用类型对象的情况除外)。

二、ThreadLocal 实现原理
底层数据结构

ThreadLocal 底层是通过ThreadLocalMap 这个静态内部类来存储数据的,ThreadLocalMap可以理解为ThreadLocal 类实现的定制化的 HashMap,它的底层是 Entry 对象数组,Entry 对象中存放的键是 ThreadLocal 对象,值是 Object 类型的具体存储内容。除此之外,ThreadLocalMap 也是 Thread 类一个属性,可以在Thread类源码中找到。

img

主要方法
1.get方法
    public T get() {
      // 获取当前线程
      Thread t = Thread.currentThread();
      // 获取 Thread 类中 ThreadLocal.ThreadLocalMap 类型的 threadLocals 变量
      ThreadLocalMap map = getMap(t);
      // 若 threadLocals 变量不为空,根据 ThreadLocal 对象来获取 key 对应的 value
      if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
          @SuppressWarnings("unchecked")
          T result = (T)e.value;
          return result;
        }
      }
      // 若 threadLocals 变量是 NULL,初始化一个新的 ThreadLocalMap 对象
      return setInitialValue();
    }

    // 初始化一个新的 ThreadLocalMap 对象
    private T setInitialValue() {
      // 初始化一个 NULL 值
      T value = initialValue();
      // 获取当前线程
      Thread t = Thread.currentThread();
      // 获取 Thread 类中 ThreadLocal.ThreadLocalMap 类型的 threadLocals 变量
      ThreadLocalMap map = getMap(t);
      if (map != null)
        map.set(this, value);
      else
        createMap(t, value);
      return value;
    }

    // ThreadLocalMap#createMap
    void createMap(Thread t, T firstValue) {
      t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

通过 ThreadLocal#get() 方法可以很清晰的看到,我们根据 ThreadLocal对象从ThreadLocal中读取数据时,首先会获取当前线程对象,然后得到当前线程对象中 ThreadLocal.ThreadLocalMap类型的 threadLocals属性;

如果 threadLocals属性不为空,会根据 ThreadLocal对象作为 key 来获取对应的 value;如果 threadLocals 变量是 NULL,就初始化一个新的ThreadLocalMap 对象。

再看 ThreadLocalMap 的构造方法,也就是 Thread 类中 ThreadLocal.ThreadLocalMap 类型的 threadLocals属性不为空时的执行逻辑。

    // 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 对象作为 key,存储的具体内容 Object 对象作为 value,包装成一个 Entry 对象,放到 ThreadLocalMap类中类型为 Entry 数组的 table 属性中,这样就完成了线程局部变量的存储。所以说, ThreadLocal 中的数据最终是存放在 ThreadLocalMap 这个类中的

2.set方法
    public void set(T value) {
      // 获取当前线程
      Thread t = Thread.currentThread();
      // 获取 Thread 类中 ThreadLocal.ThreadLocalMap 类型的 threadLocals 变量
      ThreadLocalMap map = getMap(t);
      // 若 threadLocals 变量不为空,进行赋值;否则新建一个 ThreadLocalMap 对象来存储
      if (map != null)
        map.set(this, value);
      else
        createMap(t, value);
    }

    // ThreadLocalMap#set
    private void set(ThreadLocal<?> key, Object value) {
      // 获取 ThreadLocalMap 的 Entry 数组对象
      Entry[] tab = table;
      int len = tab.length;
      // 获取当前 ThreadLocal 对象的散列值
      int i = key.threadLocalHashCode & (len-1);
      // 解决哈希冲突,线性探测法
      for (Entry e = tab[i];
           e != null;
           e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        // 代码(1)
        if (k == key) {
          e.value = value;
          return;
        }
        // 代码(2)
        if (k == null) {
          replaceStaleEntry(key, value, i);
          return;
        }
      }
      // 代码(3)将 key-value 包装成 Entry 对象放在数组退出循环时的位置中
      tab[i] = new Entry(key, value);
      int sz = ++size;
      if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
    }

    // ThreadLocalMap#nextIndex
    // Entry 数组的下一个索引,若超过数组大小则从0开始,相当于环形数组
    private static int nextIndex(int i, int len) {
      return ((i + 1 < len) ? i + 1 : 0);
    }

ThreadLocalMap 类的底层数据结构是一个 Entry类型的数组,但与 HashMap 中的 Node 数组+链表形式不同的是,Entry 类没有 next 属性来构成链表,没有像HashMap一样使用拉链法解决哈希碰撞,而是采用线性探测法

解决哈希冲突的循环中:

  • 代码(1),如果当前 ThreadLocal对象正好等于 Entry 对象中的 key 属性,直接更新 ThreadLocalvalue的值;
  • 代码(2),如果当前 ThreadLocal 对象不等于 Entry对象中的key属性,并且 Entry对象的 key 是空的,这里进行的逻辑其实是 设置键值对,同时清理无效的 Entry (防止内存泄漏);
  • 代码(3),如果在遍历中没有发现当前 TheadLocal 对象的散列值,也没有发现 Entry 对象的 key为空的情况,而是满足了退出循环的条件,即 Entry 对象为空时,那么就会创建一个 新的 Entry 对象进行存储 ,同时做一次 启发式清理 ,将 Entry 数组中 key 为空,value 不为空的对象的 value 值释放;
ThreadLocalMap扩容机制

它在扩容前有两个判断的步骤,都满足后才会进行最终扩容。

  • ThreadLocalMap#set(ThreadLocal<?> key, Object value) 方法中可能会触发启发式清理,在清理无效 Entry 对象后,如果数组长度大于等于数组定义长度的 2/3,则首先进行 rehash;

        // rehash 条件
        private void setThreshold(int len) {
          threshold = len * 2 / 3;
        }
    
  • rehash 会触发一次全量清理,如果数组长度大于等于数组定义长度的 1/2,则进行 resize(扩容);

        // 扩容条件
        private void rehash() {
          expungeStaleEntries();
    
          // Use lower threshold for doubling to avoid hysteresis
          if (size >= threshold - threshold / 4)
            resize();
        }
    
  • 进行扩容时,Entry 数组为扩容为 原来的2倍 ,重新计算 key 的散列值,如果遇到 key 为 NULL 的情况,会将其 value 也置为 NULL,帮助虚拟机进行GC。

        // 具体的扩容函数
        private void resize() {
          Entry[] oldTab = table;
          int oldLen = oldTab.length;
          int newLen = oldLen * 2;
          Entry[] newTab = new Entry[newLen];
          int count = 0;
    
          for (int j = 0; j < oldLen; ++j) {
            Entry e = oldTab[j];
            if (e != null) {
              ThreadLocal<?> k = e.get();
              if (k == null) {
                e.value = null; // Help the GC
              } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                  h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
              }
            }
          }
    
          setThreshold(newLen);
          size = count;
          table = newTab;
        }
    
父子线程间局部变量传递

实现线程间局部变量传递使用InheritableThreadLocal 类。

    public class InheritableThreadLocal<T> extends ThreadLocal<T> {

        protected T childValue(T parentValue) {
            return parentValue;
        }

        ThreadLocalMap getMap(Thread t) {
           return t.inheritableThreadLocals;
        }

        void createMap(Thread t, T firstValue) {
            t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
        }
    }

先用一个简单的示例来实践一下父子线程间局部变量的传递功能。

    public static void main(String[] args) {
      ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
      threadLocal.set("这是父线程设置的值");

      new Thread(() -> System.out.println("子线程输出:" + threadLocal.get())).start();
    }

    // 输出内容
    子线程输出:这是父线程设置的值

可以看到,在子线程中通过调用 InheritableThreadLocal#get() 方法,拿到了在父线程中设置的值。

实现父子线程间的局部变量共享需要追溯到 Thread 对象的构造方法:

    public Thread(Runnable target) {
      init(null, target, "Thread-" + nextThreadNum(), 0);
    }

    private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
      init(g, target, name, stackSize, null, true);
    }

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      // 该参数一般默认是 true
                      boolean inheritThreadLocals) {
      // 省略大部分代码
      Thread parent = currentThread();

      // 复制父线程的 inheritableThreadLocals 属性,实现父子线程局部变量共享
      if (inheritThreadLocals && parent.inheritableThreadLocals != null) {
        this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); 
      }

      // 省略部分代码
    }

在最终执行的构造方法中,有这样一个判断:如果当前父线程(创建子线程的线程)的 inheritableThreadLocals属性不为 NULL,就会将当下父线程的 inheritableThreadLocals属性复制给子线程的 inheritableThreadLocals属性。具体的复制方法如下:

    // ThreadLocal#createInheritedMap
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
      return new ThreadLocalMap(parentMap);
    }

    private ThreadLocalMap(ThreadLocalMap parentMap) {
      Entry[] parentTable = parentMap.table;
      int len = parentTable.length;
      setThreshold(len);
      table = new Entry[len];
        // 一个个复制父线程 ThreadLocalMap 中的数据
      for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
          @SuppressWarnings("unchecked")
          ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
          if (key != null) {
            // childValue 方法调用的是 InheritableThreadLocal#childValue(T parentValue)
            Object value = key.childValue(e.value);
            Entry c = new Entry(key, value);
            int h = key.threadLocalHashCode & (len - 1);
            while (table[h] != null)
              h = nextIndex(h, len);
            table[h] = c;
            size++;
          }
        }
      }
    }

需要注意的是,复制父线程共享变量的时机是在创建子线程时,如果在创建子线程后父线程再往 InheritableThreadLocal 类型的对象中设置内容,将不再对子线程可见。

三、内存泄漏分析
发生内存泄漏的原因

ThreadLocal 发生内存泄漏的原因需要从 Entry 对象说起

    // ThreadLocal->ThreadLocalMap->Entry
    static class Entry extends WeakReference<ThreadLocal<?>> {
      /** The value associated with this ThreadLocal. */
      Object value;

      Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
      }
    }

Entry对象的 keyThreadLocal 类是继承于 WeakReference 弱引用类。具有弱引用的对象有更短暂的生命周期,在发生 GC活动时,无论内存空间是否足够,垃圾回收器都会回收具有弱引用的对象。

由于 Entry 对象的 key 是继承于 WeakReference 弱引用类的,若 ThreadLocal 类没有外部强引用,当发生 GC 活动时就会将 ThreadLocal对象回收。

而此时如果创建 ThreadLocal类的线程依然活动,那么 Entry 对象中 ThreadLocal对象对应的 value 就依旧具有强引用而不会被回收,从而导致内存泄漏。

如何解决内存泄漏问题

要想解决内存泄漏问题其实很简单,只需要记得在使用完 ThreadLocal 中存储的内容后将它 remove 掉就可以了。

ThreadLocal 内部如何防止内存泄漏

ThreadLocalMap#set(ThreadLocal<?> key, Object value)其实已经有涉及 ThreadLocal 内部清理无效 Entry的逻辑了,在通过线性检测法处理哈希冲突时,若 Entry 数组的 key与当前 ThreadLocal 不是同一个对象,同时 key为空的时候,会进行清理无效 Entry 的处理,即 ThreadLOcalMap#replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) 方法:

	private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }
  • 这个方法中也是一个循环,循环的逻辑与 ThreadLocalMap#set(ThreadLocal<?> key, Object value)方法一致;
  • 在循环过程中如果找到了将要存储的 ThreadLocal 对象,则会将它与进入 replaceStaleEntry 方法时满足条件的 k 值做交换,同时将 value 更新;
  • 如果没有找到将要存储的 ThreadLocal对象,则会在此 k 值处新建一个Entry对象存储;
  • 同时,在循环过程中如果发现其他无效的EntrykeyNULLvalue还在的情况,可能导致内存泄漏,下文会有详细描述),会顺势找到 Entry 数组中所有的无效 Entry,释放这些无效 Entry(通过将 keyvalue 都设置为NULL),在一定程度上避免了内存泄漏;

如果满足线性检测循环结束条件了,即遇到了 Entry==NULL 的情况,就新建一个 Entry 对象来存储数据。然后会进行一次启发式清理,如果启发式清理没有成功释放满足条件的对象,同时满足扩容条件时,会执行 ThreadLocalMap#rehash() 方法。

    private void rehash() {
      // 全量清理
      expungeStaleEntries();
      // 满足条件则扩容
      if (size >= threshold - threshold / 4)
        resize();
    }

ThreadLocalMap#rehash() 方法中会对 ThreadLocalMap 进行一次全量清理,全量清理会遍历整个 Entry 数组,删除所有 keyNULLvalue不为 NULL 的脏 Entry对象。

    // 全量清理
    private void expungeStaleEntries() {
      Entry[] tab = table;
      int len = tab.length;
      for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        if (e != null && e.get() == null)
          expungeStaleEntry(j);
      }
    }

进行全量清理之后,如果 Entry数组的大小大于等于 threshold - threshold / 4 ,则会进行2倍扩容。

总结一下:在ThreadLocal内部是通过在 get、set、remove 方法中主动进行清理 keyNULLvalue不为 NULL 的无效 Entry 来避免内存泄漏问题。

但是基于 get、set 方法让 ThreadLocal自行清理无效 Entry对象并不能完全避免内存泄漏问题,要彻底解决内存泄漏问题还得养成使用完就主动调用remove 方法释放资源的好习惯。

四、ThreadLocal 应用场景及示例

ThreadLocal 在很多开源框架中都有应用,比如:Spring 中的事务隔离级别的实现

Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。

Spring框架里面就是用的ThreadLocal来实现这种隔离,主要是在TransactionSynchronizationManager这个类里面,代码如下所示:

private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);

	private static final ThreadLocal<Map<Object, Object>> resources =
			new NamedThreadLocal<>("Transactional resources");

	private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
			new NamedThreadLocal<>("Transaction synchronizations");

	private static final ThreadLocal<String> currentTransactionName =
			new NamedThreadLocal<>("Current transaction name");

  ……

还有就是很多场景的cookie,session等数据隔离都是通过ThreadLocal去做实现的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值