一篇文章搞懂 ThreadLocal

ThreadLocal 如何保证对象只被当前线程访问呢?

下面让我们一起深入 ThreadLocal 的内部实现。

我们需要关注的自然是 ThreadLocal 的 set() 方法和 get() 方法。

set

先从 set() 方法说起:

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="" cid="n768" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">
/**

  • Sets the current thread's copy of this thread-local variable
  • to the specified value. Most subclasses will have no need to
  • override this method, relying solely on the {@link #initialValue}
  • method to set the values of thread-locals.
  • @param value the value to be stored in the current thread's copy of
  •    this thread-local.
    

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

在 set 时,首先获得当前线程对象,然后通过 getMap() 方法拿到线程的 ThreadLocalMap ,并将值存入 ThreadLocalMap 中。

而 ThreadLocalMap 可以理解为一个 Map (虽然不是,但是你可以把它简单地理解成 HashMap ),但是它是定义在 Thread 内部的成员。

注意下面的定义是从 Thread 类中摘出来的

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="" cid="n773" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"> /* ThreadLocal values pertaining to this thread. This map is maintained

  • by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;</pre>

而设置到 ThreadLocal 中的数据,也正是写入了 threadLocals 的这个 Map 。

其中, key 为 ThreadLocal 当前对象, value 就是我们需要的值。

而 threadLocals 本身就保存了当前自己所在线程的所有“局部变量”,也就是一个 ThreadLocal 变量的集合。

在这里也给想提升的开发人们安利一个福利:Java高级进阶笔记,完整版PDF文档点击此处免费领取

get

在进行 get() 方法操作时,自然就是将这个 Map 中的数据拿出来。

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="" cid="n779" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">

/**

  • Returns the value in the current thread's copy of this
  • thread-local variable. If the variable has no value for the
  • current thread, it is first initialized to the value returned
  • by an invocation of the {@link #initialValue} method.
  • @return the current thread's value of this thread-local
    */
    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();
    }</pre>

get() 方法先取得当前线程的 ThreadLocalMap 对象,然后通过将自己作为 key 取得内部的实际数据。

Thread.exit()

在了解了 ThreadLocal 的内部实现后,我们自然会引出一个问题:

那就是这些变量是维护在 Thread 类内部的( ThreadLocalMap 定义所在类),这也意味着只要线程不退出,对象的引用将一直存在。

当线程退出时, Thread 类会进行一些清理工作,其中就包括清理 ThreadLocalMap 。

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="" cid="n784" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">
/**

  • This method is called by the system to give a Thread
  • a chance to clean up before it actually exits.
    /
    private void exit() {
    if (group != null) {
    group.threadTerminated(this);
    group = null;
    }
    /
    Aggressively null out all reference fields: see bug 4006245 /
    target = null;
    /
    Speed the release of some of these resources */
    threadLocals = null;
    inheritableThreadLocals = null;
    inheritedAccessControlContext = null;
    blocker = null;
    uncaughtExceptionHandler = null;
    }</pre>

因此,使用线程池就意味着当前线程未必会退出(比如固定大小的线程池,线程总是存在)。

如果这样,将一些大的对象设置到 ThreadLocal 中(它实际保存在线程持有的 ThreadLocalMap 内),可能会使系统出现内存泄漏的可能。

这里我的意思是:你设置了对象到 Threadlocal 中,但是不清理它,在你使用几次后,这个对象也不再有用了,但是它却无法被回收。

此时,如果你希望及时回收对象,最好使用 ThreadLocal.remove() 方法将这个变量移除,就像我们习惯性地关闭数据库连接一样。

如果你确实不需要这个对象了,就应该告诉虚拟机,请把它回收,防止内存泄漏。

tl = null

另外一种有趣的情况是 JDK 也可能允许你像释放普通变量一样释放 ThreadLocal 。

比如,我们有时候为了加速垃圾回收,会特意写出类似 obj = null 的代码。

如果这么做,那么 obj 所指向的对象就会更容易地被垃圾回收器发现,从而加速回收。

同理,如果对于 ThreadLocal 的变量,我们也手动将其设置为 null ,比如 tl = null ,那么这个 ThreadLocal 对应的所有线程的局部变量都有可能被回收。

这里面的奥秘是什么呢?

先来看一个简单的例子。

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="" cid="n789" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">package com.shockang.study.java.concurrent.thread_local;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadLocalDemo_Gc {
static volatile ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>() {
protected void finalize() throws Throwable {
System.out.println(this.toString() + " is gc");
}
};
static volatile CountDownLatch cd = new CountDownLatch(10000);

public static class ParseDate implements Runnable {
int i = 0;

public ParseDate(int i) {
this.i = i;
}

public void run() {
try {
if (tl.get() == null) {
tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") {
protected void finalize() throws Throwable {
System.out.println(this.toString() + " is gc");
}
});
System.out.println(Thread.currentThread().getId() + ":create SimpleDateFormat");
}
Date t = tl.get().parse("2015-03-29 19:29:" + i % 60);
} catch (ParseException e) {
e.printStackTrace();
} finally {
cd.countDown();
}
}
}

public static void main(String[] args) throws InterruptedException {
ExecutorService es = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10000; i++) {
es.execute(new ParseDate(i));
}
cd.await();
System.out.println("mission complete!!");
tl = null;
System.gc();
System.out.println("first GC complete!!");
//在设置ThreadLocal的时候,会清除ThreadLocalMap中的无效对象
tl = new ThreadLocal<SimpleDateFormat>();
cd = new CountDownLatch(10000);
for (int i = 0; i < 10000; i++) {
es.execute(new ParseDate(i));
}
cd.await();
Thread.sleep(1000);

System.gc();
System.out.println("second GC complete!!");

}
}</pre>

上述案例是为了跟踪 ThreadLocal 对象,以及内部 SimpleDateFormat 对象的垃圾回收。

为此,我们重载了 finalize() 方法。

这样,我们在对象被回收时,就可以看到它们的踪迹。

在主函数 main 中,先后进行了两次任务提交,每次 10000 个任务。

在第一次任务提交后,我们将 tl 设置为 null ,并进行一次 GC 。

接着,我们进行第二次任务提交,完成后,再进行一次 GC 。

执行上述代码,最有可能的一种输出如下所示。

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="" cid="n794" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">19:create SimpleDateFormat
15:create SimpleDateFormat
17:create SimpleDateFormat
18:create SimpleDateFormat
20:create SimpleDateFormat
14:create SimpleDateFormat
11:create SimpleDateFormat
12:create SimpleDateFormat
13:create SimpleDateFormat
16:create SimpleDateFormat
mission complete!!
first GC complete!!
com.shockang.study.java.concurrent.thread_local.ThreadLocalDemo_Gc$1@5041865d is gc
11:create SimpleDateFormat
14:create SimpleDateFormat
20:create SimpleDateFormat
12:create SimpleDateFormat
16:create SimpleDateFormat
13:create SimpleDateFormat
18:create SimpleDateFormat
15:create SimpleDateFormat
17:create SimpleDateFormat
19:create SimpleDateFormat
second GC complete!!</pre>

注意这些输出所代表的含义。

首先,线程池中 10 个线程都各自创建了一个 SimpleDateFormat 对象实例。

接着进行第一次 GC ,可以看到 ThreadLocal 对象被回收了(这里使用了匿名类,所以类名看起来有点怪,这个类就是开头创建的 t 对象)。

提交第 2 次任务,这次一样也创建了 10 个 SimpleDateFormat 对象,然后进行第二次 GC 。

在第二次 GC 后,第一次创建的 10 个 SimpleDateFormat 的子类实例全部被回收。

虽然我们没有手工 remove (这些对象,但是系统依然有可能回收它们)。

ThreadLocal.ThreadLocalMap

要了解上面的回收机制,我们需要更进一步了解 ThreadLocal.ThreadLocalMap 的实现。

之前我们说过, ThreadLocalMap 是一个类似 HashMap 的东西。

更准确地说,它更加类似 WeakHashMap。

ThreadLocalMap 的实现使用了弱引用。

弱引用是比强引用弱得多的引用。

Java 虚拟机在垃圾回收时,如果发现弱引用,就会立即回收。

ThreadLocalMap 内部由一系列 Entry 构成,每一个 Entry 都是 WeakReference< ThreadLocal>。

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="" cid="n799" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"> /**

  • 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;
}
}</pre>

这里的参数 k 就是 Map 的 key , v 就是 Map 的 value ,其中 k 也是 ThreadLocal 实例,作为弱引用使用。

super(k) 就是调用了 WeakReference 的构造函数

因此,虽然这里使用 ThreadLocal 作为 Map 的 key ,但是实际上,它并不真的持有 Threadlocal 的引用。

而当 ThreadLocal 的外部强引用被回收时, ThreadLocalMap 中的 key 就会变成 null 。

当系统进行 ThreadLocalMap 清理时(比如将新的变量加入表中,就会自动进行一次清理,虽然 JDK 不定会进行一次彻底的扫描,但显然在这个案例中,它奏效了),就会将这些垃圾数据回收。

ThreadLocal 的回收机制

ThreadLocal 的回收机制,如图所示。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容