ThreadLocal

ThreadLocal,先看名字,thread - 线程,local - 本地,组合起来为线程本地变量

JDK 中注释如下:


 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).

ThreadLocal 应用场景

通过 TheadLocal 为当前线程设置变量值,在任何需要用到变量值时,通过 ThreadLocal.get() 获取即可。

一般来说适合使用 ThreadLocal 要满足两个条件。

  • 1、每个线程私有的变量
  • 2、需要在不同的地方获取

使用场景如下:

在一段业务逻辑开始时,set 一个值,在业务后续处理中将值 get 获得,不用在层层调用的各个方法中通过入参来显式传递。

  • 1、用户请求后台操作时,将用户信息存储到 ThreadLocal 中,经过多重拦截器去进行权限校验,在每个拦截器中都通过 ThreadLocal 获取到用户信息。
  • 2、链路追踪,在逻辑执行前生成一个 traceId,通过 ThreadLocal 保存,每段业务逻辑执行打印日志时从 ThreadLocal 中获取 traceId,保证同一个线程的操作日志可以方便追溯。

在很多开源框架中,ThreadLocal 经常被使用。


public class ThreadLocalTest {

    static ThreadLocal<Integer> context = new ThreadLocal<>();

    public static void main(String[] args) {
        ThreadLocalTest threadLocalTest = new ThreadLocalTest();

        new Thread(() -> {
            int id = (int) (Math.random() * 10000);
            context.set(id);
            System.out.println(Thread.currentThread().getName() +
                    ", 开始执行 id= " + id);
            threadLocalTest.test1();
        }).start();

        new Thread(() -> {
            int id = (int) (Math.random() * 10000);
            context.set(id);
            System.out.println(Thread.currentThread().getName() +
                    ", 开始执行 id= " + id);
            threadLocalTest.test1();
        }).start();

    }

    private void test1() {
        test2();
    }

    private void test2() {
        test3();
    }

    private void test3() {
        test4();
    }

    private void test4() {
        test5();
    }

    private void test5() {
        System.out.println(Thread.currentThread().getName() + "," +
                " 执行完毕 id= " + context.get());
        context.remove();
    }
}

输出:


Thread-0, 开始执行 id= 5384
Thread-0, 执行完毕 id= 5384
Thread-1, 开始执行 id= 2235
Thread-1, 执行完毕 id= 2235

ThreadLocal 解析

ThreadLocalget()set() 方法 ,实际操作的都是 Thread.currentThread() ,即当前线程的 threadLocals 变量。

threadLocals 变量结构是一个 map 结构 —— ThreadLocalMap

ThreadLocalMap 底层是一个 Entry 数组, Entrykey / value 组成。

key 为当前 ThreadLocal, valueset 的值。


public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //从 map 中以 threadLocal 为 key 获取
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //如果 map 为 null,则初始化一个
    return setInitialValue();
}

public void set(T value) {
    Thread t = Thread.currentThread();
    // 当前线程的 threadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //向 map 中 set
        map.set(this, value);
    else
        createMap(t, value);
}

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

map 出现 hash 冲突时,使用的并不是 hashMap 的拉链法,而是使用的线性探测法,向下一个位置寻找。


private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    //取 hashCode , 和 数组大小 取余,找到槽位
    int i = key.threadLocalHashCode & (len-1);
    //循环,
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        //如果槽位不为 null, 判断是否是自己,如果是,则覆盖 value
        if (k == key) {
            e.value = value;
            return;
        }
        // 如果 key 是 null,则替换
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
        //如果不为null,则 i = nextIndex, + 1 ,到尾部再从 0 开始
        //找到槽位不为空,则向后寻找槽位
    }

    // 新建一个 entry,放入槽位
    tab[i] = new Entry(key, value);
    // size + 1
    int sz = ++size;
    // 清空一些槽位,如果没有可清空的,并且大于扩容阈值,则扩容 rehash
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

ThreadLocalMapkeyweakReference 弱引用类型,当 key 没有 threadLocal 强引用时,GC 时被回收,一定程度上解决内存泄漏问题。

但此时 value 不是 null,仍然会引起内存泄漏,所以 get set remove 方法都会对 key == null 的槽位进行清理回收。


static class ThreadLocalMap {
    
    //内部类,entry , 由 key value 组成, key 是 threadLocal本身
    //key 是 弱引用
    static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

    /**
     * The table, resized as necessary.
     * table.length MUST always be a power of two.
     */
    //是数组结构, 并没有链表结构,重点分析 hash 冲突如何解决
    private Entry[] table;
}

最佳实践

ThreadLocal 使用时必须显示地调用 remove 方法来避免内存泄漏。
如果使用了线程池,不及时 remove,会造成数据错乱的问题。