[译]Java 的 ThreadLocal 底层实现理念探究

108 阅读4分钟

这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战

前言

在上一篇文章中,我对于ThreadLocal的实现理念介绍的比较粗浅,所以今天我找来了一篇StackOverFlow的回答,我们一起来探究它的实现理念。

译文

所有在这里的答案都是正确的,但是有一点令人失望的就是没有展示出ThreadLocal实现的聪明之处,我查看了ThreadLocal的源码,并且对它的实现方式印象深刻。

Java 原生的实现

假如我要求你根据ThreadLocal提供Java Doc的API描述去实现一个相同功能的类,你会怎么做呢?一个非常容易想到的实现应该是使用ConcurrentHashMap<Thread, T>,将Thread.currentThread()作为它的key值。这个实现运行起来非常合理,但是会存在一些缺点。

  • 线程竞争,ConcurrentHashMap 是一个非常智能的类,但它最终仍然必须处理防止多个线程以任何方式处理它,如果不同的线程去频繁的去使用它,ConcurrentHashMap的速度将会变慢。
  • 永久保留指向 Thread 和对象的指针,即使在 Thread 完成并可能被 GC 处理之后也是如此。

GC 友好的实现

Ok try again, lets deal with the garbage collection issue by using weak references. Dealing with WeakReferences can be confusing, but it should be sufficient to use a map built like so: 好的再一次尝试,使用弱引用(weak references)来解决垃圾回收的问题。处理 WeakReferences 方式可能会令人困惑,但使用以下的代码去构建一个Map就足够了:

Collections.synchronizedMap(new WeakHashMap<Thread, T>())

或者我们可以使用Guava(代码如下):

new MapMaker().weakKeys().makeMap()

这意味着一旦没人持有线程(暗示着线程已经结束了)对应的key/value可以被垃圾回收,这是一个改善,但是依旧没有解决线程竞争的问题,意味着目前为止我们自己实现的ThreadLocal不是非常的不可以思议。更进一步说,假如有人决定持有已经结束Thread对象。那么这些对象将再也不会被GC回收,因此我们的对象也不会的,即使是他们在技术上是不可访问的。

巧妙的实现

We've been thinking about ThreadLocal as a mapping of threads to values, but maybe that's not actually the right way to think about it. Instead of thinking of it as a mapping from Threads to values in each ThreadLocal object, what if we thought about it as a mapping of ThreadLocal objects to values in each Thread? If each thread stores the mapping, and ThreadLocal provides a nice interface into that mapping, we can avoid all of the issues of the previous implementations.

我们一直思考着实现ThreadLocal的映射的方式为线程作为key,对应的value作为值,但是这个可能是个错误的思考方式,相反的假如我们把对应ThreadLocal对象作为Key值,value值是线程呢?每个线程的内部存放着映射,TheadLocal仅仅只是提供获取映射对应值的接口。我们就可以避免之前实现所带来的问题了。

这个实现的方式可能会像这样:

// 在每个线程的内部调用, 通过ThreadLocal实例进行更新。
new WeakHashMap<ThreadLocal,T>()

我们不需要担心并发,因为只有一个线程才可以获取这个map。

The Java devs have a major advantage over us here - they can directly develop the Thread class and add fields and operations to it, and that's exactly what they've done. Java的开发者比我们有天然的优势,他们可以直接修改Thread类,并且向其添加新的字段。以下就是他们所做的操作

 java.lang.Thread 部分代码:

// 与此线程有关的 ThreadLocal 值,这个map由ThreadLocal类进行维护。
ThreadLocal.ThreadLocalMap threadLocals = null;

正如注释所说明的那样,这确实是由 ThreadLocal 对象为此 Thread 跟踪的所有值的包私有映射。 ThreadLocalMap 的实现不是WeakHashMap,但它遵循相同的基本约定,包括通过弱引用来保存其键。

ThreadLocal.get() 实现就像以下这样:

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

 ThreadLocal.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;
}

本质上,在线程内部使用一个map去保存所有的我们的ThreadLocal对象。这种方式,我们不需要去担心其他线程回去获取我们的值,不存在并发竞态条件的问题,因为只有当前线程才可以获取的到。更进一步说,一旦Thread执行完毕后,所有的本地化的局部变量都会被垃圾回收清理。即使 Thread 被持有,ThreadLocal 对象也被弱引用持有,并且可以在 ThreadLocal 对象超出范围时被清除。

结论

通过这篇文章,想必大家已经了解到了ThreadLocal的底层实现理念了。

参考文献

How is Java's ThreadLocal implemented under the hood?