

12.ThreadLocal的那点小秘密 - 王有志
source link: https://www.cnblogs.com/wyz1994/p/17076447.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

大家好,我是王有志。关注王有志,一起聊技术,聊游戏,聊在外漂泊的生活。
好久不见,不知道大家新年过得怎么样?有没有痛痛快快得放松?是不是还能收到很多压岁钱?好了,话不多说,我们开始今天的主题:ThreadLocal。
我收集了4个面试中出现频率较高的关于ThreadLocal的问题:
- 什么是ThreadLocal?什么场景下使用ThreadLocal?
- ThreadLocal的底层是如何实现的?
- ThreadLocal在什么情况下会出现内存泄漏?
- 使用ThreadLocal要注意哪些内容?
我们先从一个“谣言”开始,通过分析ThreadLocal的源码,尝试纠正“谣言”带来的误解,并解答上面的问题。
流传已久的“谣言”
很多文章都在说“ThreadLocal通过拷贝共享变量的方式解决并发安全问题”,例如:

这种说法并不准确,很容易让人误解为ThreadLocal会拷贝共享变量。来看个例子:
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 1000; i++) { new Thread(() -> { try { System.out.println(DATE_FORMAT.parse("2023-01-29")); } catch (ParseException e) { e.printStackTrace(); } }).start(); }}
我们知道,多线程并发访问同一个DateFormat实例对象会产生严重的并发安全问题,那么加入ThreadLocal是不是能解决并发安全问题呢?修改下代码:
/** * 第一种写法 */private static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = new ThreadLocal<>() { @Override protected DateFormat initialValue() { return DATE_FORMAT; }}; public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 1000; i++) { new Thread(() -> { try { System.out.println(DATE_FORMAT_THREAD_LOCAL.get().parse("2023-01-29")); } catch (ParseException e) { e.printStackTrace(); } }).start(); }}
估计会有很多小伙伴会说:“你这么写不对!《阿里巴巴Java开发手册》中不是这么用的!”。把书中的用法搬过来:
/** * 第二种写法 */private static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = new ThreadLocal<>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); }};
Tips:代码小改了一下~~
我们来看两种写法的差别:
- 第一种写法,
ThreadLocal#initialValue
时使用共享变量DATE_FORMAT
; - 第二种写法,
ThreadLocal#initialValue
时创建SimpleDateFormat对象。
按照“谣言”的描述,第一种写法会拷贝DATE_FORMAT
的副本提供给不同的线程使用,但从结果上来看ThreadLocal并没有这么做。
有的小伙伴可能会怀疑是因为DATE_FORMAT_THREAD_LOCAL
线程共享导致的,但别忘了第二种写法也是线程共享的。
到这里我们应该能够猜到,第二种写法中每个线程会访问不同的SimpleDateFormat实例对象,接下来我们通过源码一探究竟。
ThreadLocal的实现
除了使用ThreadLocal#initialValue
外,还可以通过ThreadLocal#set
添加变量后再使用:
ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<>();threadLocal.set(new SimpleDateFormat("yyyy-MM-dd"));System.out.println(threadLocal.get().parse("2023-01-29"));
Tips:这么写仅仅是为了展示用法~~
使用ThreadLocal非常简单,3步就可以完成:
无参构造器没什么好说的(空实现),我们从ThreadLocal#set
开始。
ThreadLocal#set的实现
ThreadLocal#set
的源码:
public void set(T value) {, Thread t = Thread.currentThread(); // 获取当前线程的ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) { // 添加变量 map.set(this, value); } else { // 初始化ThreadLocalMap createMap(t, value); }}
ThreadLocal#set
的源码非常简单,但却透露出了不少重要的信息:
- 变量存储在ThreadLocalMap中,且与当前线程有关;
- ThreadLocalMap应该类似于Map的实现。
接着来看源码:
public class ThreadLocal<T> { ThreadLocalMap getMap(Thread t) { return t.threadLocals; } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }} public class Thread implements Runnable { ThreadLocal.ThreadLocalMap threadLocals = null;}
很清晰的展示出ThreadLocalMap与Thread的关系:ThreadLocalMap是Thread的成员变量,每个Thread实例对象都拥有自己的ThreadLocalMap。
另外,还记得在关于线程你必须知道的8个问题(上)提到Thread实例对象与执行线程的关系吗?
如果从Java的层面来看,可以认为创建Thread类的实例对象就完成了线程的创建,而调用
Thread.start0
可以认为是操作系统层面的线程创建和启动。
可以近似的看作是:实例对象执行线程Thread实例对象≈执行线程。也就是说,属于Thread实例对象的ThreadLocalMap也属于每个执行线程。
基于以上内容,我们好像得到了一个特殊的变量作用域:属于线程。
Tips:
- 实际上属于线程也即是属于Thread实例对象,因为Thread是线程在Java中的抽象;
- ThreadLocalMap属于线程,但不代表存储到ThreadLocalMap的变量属于线程。
ThreadLocalMap的实现
ThreadLocalMap是ThreadLocal的内部类,代码也不复杂:
public class ThreadLocal<T> { private final int threadLocalHashCode = nextHashCode(); static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } private Entry[] table; private int size = 0; private int threshold; private void setThreshold(int len) { threshold = len * 2 / 3; } 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的特点:
- ThreadLocalMap底层存储结构是Entry数组;
- 通过ThreadLocal的哈希值取模定位数组下标;
- 构造方法添加变量时,存储的是原始变量。
很明显,ThreadLocalMap是哈希表的一种实现,ThreadLocal作为Key,我们可以将ThreadLocalMap看做是“简版”的HashMap。
Tips:
- 本文不讨论哈希表实现中处理哈希冲突,数组扩容等问题的方式;
- 也不需要关注
ThreadLocalMap#set
和ThreadLocalMap#getgetEntry
的实现; - 与构造方法一样,
ThreadLocalMap#set
中存储的是原始变量。
到目前为止,无论是ThreadLocalMap#set
还是ThreadLocalMap的构造方法,都是存储原始变量,没有任何拷贝副本的操作。也就是说,想要通过ThreadLocal实现变量在线程间的隔离,就需要手动为每个线程创建自己的变量。
ThreadLocal#get的实现
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();}
前面的部分很容易理解,我们看map == null
时调用的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); } if (this instanceof TerminatingThreadLocal) { TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this); } return value;}
ThreadLocal#setInitialValue
方法几乎和ThreadLocal#set
一样,但变量是通过ThreadLocal#initialValue
获得的。如果是通过ThreadLocal#initialValue
添加变量,在第一次调用ThreadLocal#get
时将变量存储到ThreadLocalMap中。
ThreadLocal的原理
好了,到这里我们已经可以构建出对ThreadLocal比较完整的认知了。我们先来看ThreadLocal,ThreadLocalMap和Thread三者之间的关系:

可以看到,ThreadLocal是作为ThreadLocalMap中的Key的,而ThreadLocalMap又是Thread中的成员变量,属于每一个Thread实例对象。忘记ThreadLocalMap是ThreadLocal的内部类这层关系,整体结构就会非常清晰。
创建ThreadLocal对象并存储数据时,会为每个Thread对象创建ThreadLocalMap对象并存储数据,ThreadLocal对象作为Key。在每个Thread对象的生命周期内,都可以通过ThreadLocal对象访问到存储的数据。
到底是“谣言”吗?
那么“ThreadLocal通过拷贝共享变量的方式解决并发安全问题”是“谣言”吗?
我认为是的。ThreadLoal不会拷贝共享变量,它能“解决”并发安全问题的原理很简单,要求开发者为每个线程“发”一个变量,即变量本身就是线程隔离的。接近于以下写法:
public static Date parseDate(String dateStr) throws ParseException { return new SimpleDateFormat("yyyy-MM-dd").parse(dateStr);}
那这还能算是ThreadLocal去解决并发安全问题吗?
Tips:Stack Overflow上也有关于“谣言”的讨论。
既然不是解决共享变量并发安全问题的,那么ThreadLocal有什么用?我认为最主要的功能就是跳过方法的参数列表在线程内传递参数。举个例子:Dubbo借鉴Netty的FastThreadLocal,搞了InternalThreadLocal,用来隐式传递参数。
ThreadLocal的内存泄漏
在ThreadLocalMap的源码中可以看到,Entry继承自WeakReference,并且会将ThreadLocal添加到弱引用队列中:
static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; }}
我们知道,弱引用关联的对象只能存活到下一次GC。如果ThreadLocal没有关联任何强引用,只有Entry上的弱引用的话,发生一次GC后ThreadLocal就会被回收,就会存在ThreadLocalMap上关联Entry,但Entry上没有Key的情况:

此时Value依旧关联在ThreadLocalMap上,但无法通过常规手段访问,造成内存泄漏。虽然线程销毁后会释放内存,但在线程执行期间,始终有一块无法访问的内存被占用。
避免内存泄漏
为了避免内存泄漏,Java建议设置静态ThreadLocal变量,保证一直存在与之关联的强引用:
ThreadLocal instances are typically private static fields in classes.
另外,ThreadLocal自身也做了一些努力去清除这些没有Key的Entry,如:
ThreadLocalMap#getEntry
调用ThreadLocalMap#getEntryAfterMiss
;ThreadLocalMap#set
调用ThreadLocalMap#replaceStaleEntry
。
这些方法中都会尝试清除无用的Entry,只是触发条件较为苛刻,实际作用较小。
除此之外,开发者主动调用ThreadLocal#remove
清除无用变量才是正确使用ThreadLocal的方式。
ThreadLocal的注意事项
除了需要关注ThreadLocal的内存泄漏外,我们需要关注另外一种场景:线程池中使用ThreadLocal。
通常线程池不会销毁线程,因此在线程池中使用ThreadLcoal,且没有正确执行ThreadLocal#remove
的话,线程中会一直存在ThreadLocal关联的Value,那么就需要考虑清楚,这次的ThreadLocal对下一是否还适用?
ThreadLocal的内容到这里就结束了,使用方法,实现原理,包括内存泄漏都还是比较简单的。不过有一点比较难搞,因为有太多人去写“ThreadLocal通过拷贝共享变量的方式解决并发安全问题”,导致很多人认为这是ThreadLocal的核心功能,所以无法确认坐在对面的面试官是如何理解ThreadLocal的。
我也思考了“谣言”是如何产生的,大概有两点:
第一,《阿里巴巴Java开发手册》中使用ThreadLocal解决了DateFormat的并发安全问题,表现上看是ThreadLocal的能力,实际上是开发者自身保证了每个线程使用不同的DateFormat实例对象。
第二,ThreadLocal的注释中,提到了一句“independently initialized copy of the variable.”,搞得大家以为ThreadLocal会拷贝共享变量给线程使用。
如果真的遇到了这样面试官,那只能”见人说人话“了。
好了,今天就到这里了,Bye~~
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK