大家好,我是王有志。关注王有志,一起聊技术,聊游戏,聊在外漂泊的生活。

好久不见,不知道大家新年过得怎么样?有没有痛痛快快得放松?是不是还能收到很多压岁钱?好了,话不多说,我们开始今天的主题: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实例对象\approx执行线程\)。也就是说,属于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#setThreadLocalMap#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~~

12.ThreadLocal的那点小秘密的更多相关文章

  1. SpringMVC:学习笔记(12)——ThreadLocal实现会话共享

    SpringMVC:学习笔记(12)——ThreadLocal实现会话共享 ThreadLocal ThreadLocal,被称为线程局部变量.在并发编程的情况下,使用ThreadLocal创建的变量 ...

  2. ThreadLocal终极源码剖析

    目录一.ThreadLocal1.1 源码注释1.2 源码剖析      散列算法-魔数0x61c88647      set操作    get操作    remove操作1.3 功能测试1.4 应用 ...

  3. ThreadLocal 类 的源码解析以及使用原理

    1.原理图说明 首先看这一张图,我们可以看出,每一个Thread类中都存在一个属性 ThreadLocalMap 成员,该成员是一个map数据结构,map中是一个Entry的数组,存在entry实体, ...

  4. ThreadLocal终极源码剖析-一篇足矣!

    本文较深入的分析了ThreadLocal和InheritableThreadLocal,从4个方向去分析:源码注释.源码剖析.功能测试.应用场景. 一.ThreadLocal 我们使用ThreadLo ...

  5. [Think In Java]基础拾遗4 - 并发

    第21章节 并发 1. 定义任务 任务:任务就是一个执行线程要执行的一段代码.(在C语言中就是函数指针指向的某个地址开始的一段代码) [记忆误区]:任务不是线程,线程是用来执行任务的.即任务由线程驱动 ...

  6. Java高并发情况下的锁机制优化

    本文主要讲并行优化的几种方式, 其结构如下: 锁优化 减少锁的持有时间 例如避免给整个方法加锁 1 public synchronized void syncMethod(){ 2 othercode ...

  7. DAY3-Python学习笔记

    1.元类:动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的,不是定义死了,而是可以随时随地添加的 type():查看一个类型或变量的类型又可以创建出新的类型 c ...

  8. java进阶-多线程学习笔记

    多线程学习笔记 1.什么是线程 操作系统中 打开一个程序就是一个进程 一个进程可以创建多个线程 现在系统中 系统调度的最小单元是线程 2.多线程有什么用? 发挥多核CPU的优势 如果使用多线程 将计算 ...

  9. Java关键字和基础问题

    1. Java关键字 1.1 extends和implements extends继承普通class或abstract(抽象)类(java单继承) implements多继承能力,实现interfac ...

  10. 备战-Java 并发

    备战-Java 并发 谁念西风独自凉,萧萧黄叶闭疏窗 简介:备战-Java 并发. 一.线程的使用 有三种使用线程的方法: 实现 Runnable 接口: 实现 Callable 接口: 继承 Thr ...

随机推荐

  1. <五>掌握左值引用和初识右值引用

    1:C++的引用,引用和指针的区别? 1:从汇编指令角度上看,引用和指针没有区别,引用也是通过地址指针的方式访问指向的内存 int &b=a ; 是需要将a的内存地址取出并存下来, b=20; ...

  2. 通过tkinter列出全部字体名称

    通过tkinter列出windows系统全部字体名称 通过 tkinter.font 的 families() 函数实现 import tkinter import tkinter.font # 把p ...

  3. VirtualBox 下 CentOS7 静态 IP 的配置 → 多次踩坑总结,蚌埠住了!

    开心一刻 一个消化不良的病人向医生抱怨:我近来很不正常,吃什么拉什么,吃黄瓜拉黄瓜,吃西瓜拉西瓜,怎样才能恢复正常呢? 医生沉默片刻:那你只能吃屎了 环境准备 VirtualBox 6.1 网络连接方 ...

  4. Elasticsearch Analyzer 内置分词器

    Elasticsearch Analyzer 内置分词器 篇主要介绍一下 Elasticsearch中 Analyzer 分词器的构成 和一些Es中内置的分词器 以及如何使用它们 前置知识 es 提供 ...

  5. Java 19 新功能介绍

    点赞再看,动力无限. 微信搜「程序猿阿朗 」. 本文 Github.com/niumoo/JavaNotes 和 未读代码博客 已经收录,有很多知识点和系列文章. Java 19 在2022 年 9 ...

  6. 修改linux系统时间

    在Linux系统中,可以用date命令来显示或设定系统的日期与时间 1. 查看系统时间 [root@iZ2ze0gm3scdypc0i15r8yZ ~]# date Tue Aug 16 00:10: ...

  7. HMM算法python实现

    基础介绍,后5项为基础5元素 Q = ['q0', 'q1', 'q2', 'q3'] # 状态集合 States,共 N 种状态 V = ['v0', 'v1'] # 观测集合 Observatio ...

  8. C#11之原始字符串

    最近.NET7.0和C#11相继发布,笔者也是第一时间就用上了C#11,其中C#11的有一个更新能解决困扰我多年的问题,也就是文章的标题原始字符串. 在使用C#11的原始字符串时,发现的一些有意思的东 ...

  9. 视频服务HDR Vivid 还原色彩,让所见成“真”

    如今,视频正在以一种前所未有的方式渗入日常生活,是当下人们记录生活最热门的方式.所以,用户对视频的画质要求越来越高,App想要吸引更多的用户,拥有视频画质新技术的强力支撑很关键. HDR(High-D ...

  10. 【深入浅出 Yarn 架构与实现】3-2 Yarn Client 编写

    上篇文章介绍了编写 Yarn Application 的整体框架流程,本篇文章将详细介绍其中 Client 部分的编写方式. 一.Yarn Client 编写方法 本篇代码已上传 Github: Gi ...