15 Java面试必问:ThreadLocal的使用及原理
Diego38 60 1

1. 前言

之前讲过 as-if-serial 语义,即不管怎么重排序,在单线程内的执行结构不会被改变,单线程内不会遇到并发安全问题。因此如果变量的修改和读取可以在单线程内封闭解决,那就不会出现并发问题,比如通过拆分不同的队列,使得同一用户的数据只会分发到相同队列中,每个队列对应一个线程顺序消费。

Java 中提供了一个 ThreadLocal 组件,它可以将变量绑定到一个 Thread 对象上,线程内执行的方法都能方便取出和设置变量,而不用进行并发控制。ThreadLocal 是工程师必须要掌握的知识点,在面试中经常出现,通过本节学习就能彻底掌握 ThreadLocal 的使用和原理。

2. ThreadLocal 的使用

ThreadLocal 提供线程内的变量存储,通过将变量绑定到线程 Thread 对象上,单个线程内可以共享和设置这些变量;变量在线程之间相互隔离,互不影响;线程终止后,绑定到线程对象上变量会被自动垃圾回收。

可以通过 threadLocal.set (value) 来设置一个值,再通过 get () 方法获取先前设置的值。

ThreadLocal 往往被定义为静态的,这样做的好处是当线程内方法进行静态方法获取;利用 ThreadLocal 避免了方法间的参数传递,转而通过静态方法读取和设置。

下面的例子演示了一个用户购物的过程:

public class ThreadLocalTest {

    public static final ThreadLocal<Long> UID_HOLDER = new ThreadLocal<>();
    //有初始值的情况,可以使用以下语句构建
    //public static final ThreadLocal<Long> UID_HOLDER = ThreadLocal.withInitial(() -> ThreadLocalRandom.current().nextLong());

    /**
     * 一副太阳镜150元
     * @param count
     * @return
     */
    public static Long payGlasses(Integer count) {
        return count * 150L;
    }

    /**
     * 一瓶酒200元,但要求购买人年满21岁
     * @param count
     * @return
     */
    public static Long payAlcohol(Integer count) {
        //得到购买人的用户信息
        Long uid = UID_HOLDER.get();
        if (uid == null) {
            throw new IllegalArgumentException();
        }
        Integer age = getAge(uid);
        System.out.println("用户" + UID_HOLDER.get() + "年满" + age);
        if (age < 21) {
            throw new IllegalStateException("未到饮酒年龄");
        }
        return count * 200L;
    }

    public static Integer getAge(Long uid) {
         return Math.abs(uid.hashCode() % 80);
    }

    public static void main(String[] args) {
        UID_HOLDER.set(898989898L);
        Long fee = payAlcohol(2);
        Long fee2 = payGlasses(3);
        Long amount = fee + fee2;
        System.out.println("用户" + UID_HOLDER.get() + "共花费" + amount);
        //执行remove方法方便下次使用
        UID_HOLDER.remove();
    }
}

在上述代码中,将用户信息存放在 ThreadLocal 中,在执行 payAlcohol 时通过 ThreadLocal 获取事先设置好的用户 UID 变量,进而判断用户是否到了法定购酒年龄。

ThreadLocal 可以通过 withInitial 方法支持初始化变量;ThreadLocal 常常被用在处理用户请求流程中,在使用完之后,调用 remove 清理变量,避免污染下个用户请求,是使用 ThreadLocal 的最佳实践。

3. ThreadLocal 的实现原理

ThreadLocal 的实现原理是面试高频考察的知识点,常常会有几下几个追问的问题:

  • ThreadLocal 的数据结构是怎么样的?
  • ThreadLocal 如何实现线程封闭,线程间变量隔离?
  • 线程消亡后,变量怎么回收的?
  • ThreadLocal 和弱引用有什么联系?

以上问题先思考五分钟,带着问题去学习才更有动力。

ThreadLocal 的两个核心方法,get 和 set,我们看到都会指向 Thread 对象的 ThreadLocalMap:

  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();
    }

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }


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

线程对象 Thread 内部有一个 ThreadLocalMap,它属于 ThreadLocal 类的内部类。

   ThreadLocal.ThreadLocalMap threadLocals = null;

而 ThreadLocalMap 内部存放着以 ThreadLocal 为 Key,Object 为 value 的 Map。

    static class ThreadLocalMap {

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

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

由此我们得出 ThreadLocal 的数据结构

3.1 ThreadLocal 的数据结构

以下是 Thread、ThreadLocal、ThreadLocalMap 的引用关系: image

在图示中,thread1 ~ thread3 起初各自都关联不同的实例 ThreadLocalMap,threadLocalMap 内部以 ThreadLocal 的弱引用为 key,以 ThreadLocal 存放的 value 为 Map.Entry 的 value;;thread2 消亡后,随着 ThreadLocalMap 的引用也被释放。

  • 在调用 threadlocal 方法的线程存活期间,thread 里要引用 threadlocalmap,threadlocalmap 要引用 threadlocal,这样才能通过线程找到 threadlocal 对应的 value 值
  • 每个 Thread 都关联各自的 ThreadLocalMap 实例,所以实现了线程间变量存储的隔离,变量只能线程内部操作,不用做并发控制
  • 线程退出,该线程对应的 ThreadLocal 的应用也自动消失

到此,前三个问题我们已经解决,从图上看到 ThreadLocal 被定义的类实例应用着,而 ThreadLocalMap 每个 Entry 的 Key 也引用着 ThreadLocal,当前者被回收后,ThreadLocalMap 中的 ThreadLocal 也应该被删除,否则 ThreadLocalMap 内部的 Entry 数量将不可控,会产生内存泄露。

3.2 ThreadLocal 中弱引用使用

结合上图的结构,编写如下代码,看看会不会造成内存泄露。

public class ThreadLocalWeakRefTest {

    public  final ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {}
                ThreadLocalWeakRefTest refTest = new ThreadLocalWeakRefTest();
                //关联到Thread.ThreadLocalMap的Entry数量会越来越多
                refTest.threadLocal.set(ThreadLocalRandom.current().nextLong());
                System.out.println(refTest.threadLocal.get());
                //未发现内存溢出,说明弱引用起了作用
            }
        }
        );
        thread.start();
        thread.join();
    }
}

代码中运行半小时也未见造成内存泄露,说明 ThreadLocaMap 内部会在 ThreadLocal 主引用消失后自动清理关联的引用。

ThreadLocal 中弱引用是通过弱引用解决这一问题的,接下来看是如何使用的。

ThreadLocalMap 内部的 Key 并非是真正的 ThreadLocal 强引用,Map.Entry 是被包装成指向 ThreadLocal 的弱引用,从代码就可以看出:

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

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

当对象 ThreadLocal 强引用释放后,弱引用会跟着释放。

弱引用的使用场景是在已知对象中已经有 referent 的强引用,为满足当下需求不得已再做一次引用,该引用在主要引用释放后无需继续引用,为了尽快垃圾回收使用弱引用。

弱引用的实现原理是 referenceHandler 线程会定期 (不一定及时) 将符合弱引用回收条件的对象放入定义的 queue 里面,可以自定义 (必然要删除 key 对应的 value),也可以使用默认的,默认的是 NULL,即立即回收。

ThreadLocalmap 里面的 entry 虽然只有 ThreadLocal 是弱引用,value 是强引用,会导致 ThreadLocal 为 null,但是 value 还在,下次新的 ThreadLocal 被 put 进来会替换 ThreadLocal 为 null 的 entry,然后 ThreadLocalmap 在线程消失后也会消失,value 也随之消失。

4. 总结

ThreadLocal 是常用的解决并发的技巧,它的设计很精巧,结合弱引用解决了多引用垃圾回收的问题。

预览图
评论区

索引目录