「周末福报」头铁的我,一头扎进了知识盲区 ThreadLocal

时间:2022-07-24
本文章向大家介绍「周末福报」头铁的我,一头扎进了知识盲区 ThreadLocal,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

今天,在思考如何多线程场景下 Kryo 数据安全问题的时候。扎进了知识盲区 ThreadLocal。「老脸一红


三连拷问

Q: 什么是 ThreadLocal

A: ThreadLocal 即线程变量,ThreadLocal 中填充的变量属于当前线程,对其他线程来说是隔离的。

Q: 如何使用 ThreadLocal

A: 使用 ThreadLocal 对象的 get() 方法, 获取当前线程使用的 Kryo 对象。

final static ThreadLocal<Kryo> kryoLocal = new ThreadLocal<Kryo>() {
  @Override
  protected Kryo initialValue() {
    Kryo kryo = new Kryo();
    kryo.setClassLoader(Thread.currentThread().getContextClassLoader());
    kryo.register(String.class);
    kryo.register(UserDTO.class, new BeanSerializer<>(kryo, UserDTO.class));
    return kryo;
  }
};

Q: 哪些使用场景

多线程场景下的线程数据安全,例如数据库链接、Session 管理等。


源码阅读

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable.

简单又稍微复杂的源码,

  1. 简单:700 多行的源码类文件,没有花里胡哨的继承一堆接口类。
  2. 稍微复杂:包含一个重要的、有点绕口的静态内部类 ThreadLocalMap

ThreadLocal 的 get 与 set 方法源码(片段)

// 获取当前线程变量方法
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
// 初始化当前线程变量对象
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}
// 根据传入的线程对象获取线程对象
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
// 初始化一个空的 ThreadLocalMap 对象
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// 设置当前线程变量
publicvoid set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

从上面的源码来看,ThreadLocal 将当前线程变量存储于 ThreadLocalMap 之中。同时,初始化后的 ThreadLocalMap 实例对象将被维护到 Thread 的 threadLocals 、inheritableThreadLocals 之中。

这样的好处在于,可以通过 t.threadLocals 得到当前线程变量。即 Thread 对象与 ThreadLocalMap 对象存在引用与被引用之间的关联。

ThreadLocalMap 源代码(片段)

// 内部类,继承弱引用类
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
// 线程变量存储数组
private Entry[] table;
// 初始化构造器,数组初始化大小等
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 引用获取对应的线程变量  
private Entry getEntry(ThreadLocal<?> key) {
  int i = key.threadLocalHashCode & (table.length - 1);
  Entry e = table[i];
  if (e != null && e.get() == key)
      return e;
  else
      return getEntryAfterMiss(key, i, e);
}
// 设置指定 ThreadLocal 引用对应的线程变量  
private void set(ThreadLocal<?> key, Object value) {
  Entry[] tab = table;
  int len = tab.length;
  int i = key.threadLocalHashCode & (len-1);

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

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

      if (k == null) {
          replaceStaleEntry(key, value, i);
          return;
      }
  }

  tab[i] = new Entry(key, value);
  int sz = ++size;
  if (!cleanSomeSlots(i, sz) && sz >= threshold)
      rehash();
}

从上面的源码来看,ThreadLocalMap 才是用来存储当前线程变量的核心,ThreadLocal 只是起到了维护 ThreadLocalMap 变量存取的作用。

实际用来存储变量的 Entry 数组对象,同时它弱引用了 ThreadLocal 对象。弱引用的存在,允许程序在运行垃圾回收器 GC 的时候,可以销毁 Entry 对象。


内存泄漏

ThreadLocal 存在内存泄漏的错误使用问题。

Entry 存在两种引用关系:

  1. key 的弱引用关系
  2. 当前 thread 对象的强引用关系

当 key 的值变成 null 的时候,GC 就无法对该 Entry 对象进行销毁处理。也就是 value 对象依然存在,这就造成了内存泄漏的问题。

解决办法

使用 ThreadLocal 后,主动执行 remove() 操作,避免出现内存溢出情况。


小结

ThreadLocal 通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。

在很多情况下,ThreadLocal 比直接使用 synchronized 同步机制解决线程安全问题更简单、更方便。

又捋了一把 ThreadLocal 基础知识。

这个周末,又一次成功“强迫”自己学习。

感谢各位小伙伴的阅读,这里是一个技术人的学习与分享。