【发布时间】:2010-11-15 04:54:32
【问题描述】:
ThreadLocal 是如何实现的?它是用 Java 实现的(使用一些从 ThreadID 到对象的并发映射),还是使用一些 JVM 挂钩来更有效地完成它?
【问题讨论】:
标签: java multithreading thread-local thread-static
ThreadLocal 是如何实现的?它是用 Java 实现的(使用一些从 ThreadID 到对象的并发映射),还是使用一些 JVM 挂钩来更有效地完成它?
【问题讨论】:
标签: java multithreading thread-local thread-static
从概念上讲,您可以将ThreadLocal<T> 视为拥有一个存储线程特定值的Map<Thread,T>,尽管这不是它的实际实现方式。
线程特定的值存储在 Thread 对象本身中;当线程终止时,线程特定的值可以被垃圾收集。
参考:JCIP
【讨论】:
这里的所有答案都是正确的,但有点令人失望,因为它们在某种程度上掩盖了 ThreadLocal 的实现有多聪明。我只是在看source code for ThreadLocal,它的实现方式给我留下了深刻的印象。
朴素的实现
如果我要求你根据 javadoc 中描述的 API 实现一个 ThreadLocal<T> 类,你会怎么做?初始实现可能是使用Thread.currentThread() 作为其键的ConcurrentHashMap<Thread,T>。这将工作得相当好,但确实有一些缺点。
ConcurrentHashMap 是一个非常聪明的类,但它最终仍要处理防止多个线程以任何方式与它发生冲突的问题,如果不同的线程定期攻击它,就会出现减速。GC 友好的实现
好的,再试一次,让我们使用weak references 处理垃圾回收问题。处理 WeakReferences 可能会令人困惑,但使用这样构建的地图就足够了:
Collections.synchronizedMap(new WeakHashMap<Thread, T>())
或者如果我们使用Guava(我们应该使用!):
new MapMaker().weakKeys().makeMap()
这意味着一旦没有其他人持有线程(暗示它已完成),键/值可以被垃圾收集,这是一种改进,但仍然没有解决线程争用问题,这意味着到目前为止我们的 @987654339 @ 并不是一个很棒的课程。此外,如果有人在完成后决定保留 Thread 对象,它们将永远不会被 GC,因此我们的对象也不会被 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'ed 并且所有本地对象都将被清理。即使Thread 被持有,ThreadLocal 对象也被弱引用持有,并且可以在ThreadLocal 对象超出范围时立即清理。
不用说,这个实现给我留下了深刻的印象,它非常优雅地解决了许多并发问题(诚然,它利用了作为核心 Java 的一部分,但这是可以原谅的,因为它是一个非常聪明的类)并且允许用于对一次只需要一个线程访问的对象进行快速且线程安全的访问。
tl;dr ThreadLocal 的实现非常酷,而且比您乍一看可能想象的更快/更智能。
如果您喜欢这个答案,您可能还会感谢我的(不太详细的)discussion of ThreadLocalRandom。
Thread/ThreadLocal 代码 sn-ps 取自 Oracle/OpenJDK's implementation of Java 8。
【讨论】:
ThreadLocal 的要求与WeakHashMap 略有不同。例如,它使用custom hashing function"* 来消除在相同线程使用连续构造的 ThreadLocals 的常见情况下的冲突*",并提供different cleanup semantics。
Thread.exit() 被调用,你会在那里看到threadLocals = null;。评论引用this bug,您可能也会喜欢阅读。
假设你要实现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 是什么?因为你只有一个threadLocals 用于一个线程,所以如果你简单地将threadLocals 作为你的ThreadLocal(例如,将threadLocals 定义为Integer),你将只有一个ThreadLocal 用于特定线程。如果你想要一个线程有多个ThreadLocal 变量怎么办?最简单的方法是把threadLocals设为HashMap,每个条目的key是ThreadLocal变量的名称,每个条目的value是ThreadLocal变量的值。有点混乱?假设我们有两个线程,t1 和 t2。它们采用相同的Runnable 实例作为Thread 构造函数的参数,并且它们都有两个ThreadLocal 变量,分别命名为tlA 和tlb。这就是它的样子。
t1.tlA
+-----+-------+
| Key | Value |
+-----+-------+
| tlA | 0 |
| tlB | 1 |
+-----+-------+
t2.tlB
+-----+-------+
| Key | Value |
+-----+-------+
| tlA | 2 |
| tlB | 3 |
+-----+-------+
请注意,这些值是由我组成的。
现在看起来很完美。但是ThreadLocal.ThreadLocalMap 是什么?为什么不直接使用HashMap?为了解决这个问题,让我们看看当我们通过ThreadLocal类的set(T value)方法设置一个值时会发生什么:
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);
}
它使用当前ThreadLocal 实例和要设置的值创建一个新的ThreadLocalMap 实例。让我们看看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();
}
全图是这样的:
【讨论】:
Java 中的 ThreadLocal 变量通过访问 Thread.currentThread() 实例持有的 HashMap 来工作。
【讨论】:
你的意思是java.lang.ThreadLocal。这很简单,真的,它只是存储在每个Thread 对象中的名称-值对的映射(参见Thread.threadLocals 字段)。 API 隐藏了实现细节,但这或多或少是它的全部。
【讨论】: