Java的ThreadLocal如何在后台实现?


Answers:


119

此处的所有答案都是正确的,但有些令人失望,因为它们多少掩盖了巧妙ThreadLocal的实现方式。我只是在寻找源代码,ThreadLocal并且对它的实现方式印象深刻。

天真的实现

如果我要求您ThreadLocal<T>根据javadoc中描述的API来实现一个类,您将怎么办?最初的实现可能是ConcurrentHashMap<Thread,T>使用Thread.currentThread()作为其密钥。这样会很好地工作,但确实有一些缺点。

  • 线程争用-ConcurrentHashMap是一个非常聪明的类,但是它最终仍必须处理防止多个线程以任何方式破坏它,并且如果不同的线程有规律地命中它,将会降低速度。
  • 即使在线程完成并且可以进行GC处理之后,它仍永久保持指向线程和对象的指针。

GC友好的实现

好的,再试一次,让我们使用弱引用来处理垃圾回收问题。处理WeakReferences可能会造成混淆,但是使用像这样构建的地图应该足够了:

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

或者,如果我们使用的是番石榴(应该是!):

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

这意味着一旦没有其他人抓住线程(暗示线程已完成),就可以对键/值进行垃圾收集,这是一种改进,但仍无法解决线程争用问题,这意味着到目前为止,我们ThreadLocal还不是全部令人惊叹的一堂课。此外,如果有人决定在Thread完成后保留对象,则永远不会进行GC处理,因此即使我们的对象现在在技术上无法访问,我们也不会对其进行处理。

聪明的实现

我们一直在考虑ThreadLocal将线程映射为值,但实际上这并不是正确的思考方式。与其将其视为从Threads到每个ThreadLocal对象中的值的映射,不如将其视为ThreadLocal对象到每个Thread中的值的映射怎么办?如果每个线程都存储了该映射,而ThreadLocal仅提供了到该映射的一个不错的接口,我们可以避免先前实现中的所有问题。

一个实现看起来像这样:

// called for each thread, and updated by the ThreadLocal instance
new WeakHashMap<ThreadLocal,T>()

此处无需担心并发性,因为只有一个线程将访问此映射。

Java开发人员比我们这里拥有一个主要优势-他们可以直接开发Thread类并为其添加字段和操作,而这正是他们所做的。

其中java.lang.Thread有以下几行:

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
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;
}

本质上,在此线程中使用地图可以容纳我们所有的ThreadLocal对象。这样,我们永远不必担心其他线程中的值(ThreadLocal字面上只能访问当前线程中的值),因此没有并发问题。此外,一旦Thread完成,其映射将自动进行GC处理,并且将清除所有本地对象。即使Thread保留了该ThreadLocal对象,这些对象也会被弱引用保留,并且只要ThreadLocal对象超出范围,就可以对其进行清理。


不用说,这种实现方式给我留下了深刻的印象,它很好地解决了许多并发问题(可以利用作为核心Java的一部分,但这是可以原谅的,因为它是一个非常聪明的类)并且允许快速和对对象的线程安全访问,一次只需要一个线程即可访问。

tl; dr ThreadLocal的实现非常酷,并且比您乍看之下想像的更快/更智能。

如果您喜欢这个答案,您可能也会喜欢我(不那么详细)的讨论ThreadLocalRandom

Thread/ThreadLocal代码段摘自Oracle / OpenJDK的Java 8实现


1
您的答案看起来很棒,但现在对我来说太长了。+1并被接受,我将其添加到我的getpocket.com帐户中以供日后阅读。谢谢!
ripper234

我需要类似ThreadLocal的东西,也让我访问值的完整列表,就像map.values()一样。因此,我的幼稚实现是WeakHashMap <String,Object>,其中的键是Thread.currentThread()。getName()。这避免了对线程本身的引用。如果线程消失,则不再拥有该线程的名称(我承认这是一个假设),我的值也会消失。
bmauter

实际上,我是最近才回答这个问题的。AWeakHashMap<String,T>引入了一些问题,它不是线程安全的,并且“主要用于与equals方法使用==运算符测试对象标识的键对象一起使用”-因此实际上将Thread对象用作键会更好。我建议使用上面针对您的用例描述的Guava弱键映射。
dimo414

1
好了,弱键不是必需的,但是考虑在同步的HashMap上使用ConcurrentHashMap-前者是为多线程访问而设计的,并且在每个线程通常访问不同的键的情况下,效果会更好。
dimo414

1
@shmosel该类经过了高度调优,因此我将从假设已经考虑了这一点开始。快速浏览一下即可看到当线程正常终止时Thread.exit(),您将threadLocals = null;在那里看到。注释引用了该错误,您可能也喜欢阅读该错误
dimo414 '16

33

你是说java.lang.ThreadLocal。这非常简单,实际上,它只是每个Thread对象内部存储的名称/值对的映射(请参见Thread.threadLocals字段)。API隐藏了该实现细节,但这或多或少就是它的全部。


我不明白为什么需要这样做,因为根据定义,数据仅对单个线程可见。
skaffman

8
正确,在ThreadLocalMap周围或内部没有同步或锁定,因为它仅在线程中访问。
科恩

8

Java中的ThreadLocal变量通过访问Thread.currentThread()实例持有的HashMap来工作。


这是不正确的(或者至少现在不再了)。Thread.currentThread()是Thread.class中的本机调用。线程也有一个“ ThreadLocalMap”,它是一个哈希值的单桶(数组)。该对象不支持Map接口。
user924272 2014年

1
基本上就是我所说的。currentThread()返回一个Thread实例,该实例保存ThreadLocals到值的映射。
克里斯·韦斯特

4

假设您要实现ThreadLocal,如何使其成为线程特定的?当然,最简单的方法是在Thread类中创建一个非静态字段,我们称之为threadLocals。因为每个线程都由一个线程实例表示,所以threadLocals每个线程也将有所不同。这也是Java的作用:

/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

这是ThreadLocal.ThreadLocalMap什么 因为您只有一个threadLocalsfor线程,所以如果简单地将其threadLocals作为ThreadLocal(例如,将threadLocals定义为Integer),则ThreadLocal对于特定线程将只有一个。如果您想ThreadLocal为一个线程使用多个变量怎么办?最简单的方法是制作threadLocals一个HashMapkey每个条目的表示ThreadLocal变量的名称value,每个条目的表示ThreadLocal变量的值。有点困惑?假设我们有两个线程,t1t2。它们采用与构造函数Runnable参数相同的实例Thread,并且都具有两个ThreadLocal名为tlA和的变量tlb。这就是它的样子。

t1文件

+-----+-------+
| Key | Value |
+-----+-------+
| tlA |     0 |
| tlB |     1 |
+-----+-------+

t2文件

+-----+-------+
| Key | Value |
+-----+-------+
| tlA |     2 |
| tlB |     3 |
+-----+-------+

请注意,这些值是由我组成的。

现在看来很完美。但是什么ThreadLocal.ThreadLocalMap呢?为什么不只使用HashMap呢?为了解决该问题,让我们看看通过类的set(T value)方法设置值时会发生什么ThreadLocal

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

getMap(t)简单地返回t.threadLocals。因为t.threadLocals已初始化为null,所以我们createMap(t, value)首先输入:

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

ThreadLocalMap使用当前ThreadLocal实例和要设置的值创建一个新实例。让我们看看是什么ThreadLocalMap样的,它实际上是ThreadLocal课程的一部分

static class ThreadLocalMap {

    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

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

    ...

    /**
     * Construct a new map initially containing (firstKey, firstValue).
     * ThreadLocalMaps are constructed lazily, so we only create
     * one when we have at least one entry to put in it.
     */
    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);
    }

    ...

}

ThreadLocalMap该类的核心部分是Entry class,它是扩展的WeakReference。它确保如果当前线程退出,将自动进行垃圾回收。这就是为什么它使用ThreadLocalMap而不是简单的HashMap。它传递当前ThreadLocal值及其值作为Entry类的参数,因此当我们想要获取值时,可以从table作为Entry类实例的,获取它:

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

这就是整个图片:

整个图片


-1

从概念上讲,您可以将aThreadLocal<T>视为Map<Thread,T>存储线程特定值的a,尽管这并不是实际实现的方式。

特定于线程的值存储在Thread对象本身中。当线程终止时,可以专有地收集线程特定的值。

参考:JCIP


1
从概念上讲,是的。但是从上面的其他答案可以看出,实现是完全不同的。
Archit
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.