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

好久不见,不知道大家新年过得怎么样?有没有痛痛快快得放松?是不是还能收到很多压岁钱?好了,话不多说,我们开始今天的主题: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. node 学习笔记 模块和包的管理与使用

    1.前言 对于各种编程语言,代码组织是很重要的.而模块是node中的代码组织机制,node中的很多功能都以模块划分,而模块中又封装了许多方法,而且不会改变全局作用域,极大的方便了各开发者的需求. 2. ...

  2. 【lwip】10-ICMP协议&源码分析

    目录 前言 10.1 ICMP简介 10.2 ICMP报文 10.2.1 ICMP报文格式 10.2.2 ICMP报文类型 10.2.3 ICMP报文固定首部字段意义 10.3 ICMP差错报告报文 ...

  3. 畅联云平台(www.24hlink.cn)支持的用传列表

    无锡蓝天 沈阳君丰 无锡富贝 海康威视 海湾 苏州思迪 法安通 北大青鸟 金盾 依爱 威隆 1)几乎集齐了市场上常见的用户信息传输装置的类型,如果没接入的,我们也能接入哦. 2)欢迎咨询我们关于用传的 ...

  4. LAL v0.32.0发布,更好的支持纯视频流

    Go语言流媒体开源项目 LAL 今天发布了v0.32.0版本.距离上个版本刚好一个月时间,LAL 依然保持着高效迭代的状态. LAL 项目地址:https://github.com/q19120177 ...

  5. 【网络】内网穿透方案&FRP内网穿透实战(基础版)

    目录 前言 方案 方案1:公网 方案2:第三方内网穿透软件 花生壳 cpolar 方案3:云服务器做反向代理 FRP简介 FRP资源 FRP原理 FRP配置教程之SSH 前期准备 服务器配置 下载FR ...

  6. Linux禁止摄像头自动曝光(手动调节曝光)

    前言 很多摄像头具有自动曝光的功能,例如在较暗的调节下,提高曝光率,在较亮的调节下降低曝光.下面简单介绍在linux平台俩种方式来修改自动曝光. 软件调节(图形化界面) 安装qv4l2 sudo ap ...

  7. 关于CSDN获取博客内容接口的x-ca-signature签名算法研究

    前言 源码下载 不知道怎么就不通过了,这篇文章放出去几个月了,然后突然告诉我不行了,所以我打算换个平台(至少不能在一棵树吊死),垃圾审核 我最初想直接获取html博客,然后保存在本地,最后发布到别的博 ...

  8. 【企业流行新数仓】Day03:SuperSet图表,Ranger权限、脱敏、行级别过滤,Atlas元数据、查询和查看全表/字段血缘依赖,Zabbix告警

    一.SuperSet-图表展示 1.概念 (1)概念 通过dashboard(仪表盘)对图表中的数据进行展示 BI工具:根据配置的要求,进行数据源的配置即可 是准商业级别的BI web应用 (2)原理 ...

  9. http 缓存 笔记

    http 缓存,有时候静态资源没更新的情况下,不需要每次都去服务器获取,减少资源的请求. Http 报文中与缓存相关的首部字段 1. 通用首部字段(就是请求报文和响应报文都能用上的字段) 2. 请求首 ...

  10. Agileboot 1.6.0 发布啦 - 一款致力于规范/精简/可维护 的Springboot + Vue3的快速开发脚手架

    平台简介 AgileBoot是一套开源的全栈精简快速开发平台,毫无保留给个人及企业免费使用.本项目的目标是做一款精简可靠,代码风格优良,项目规范的小型开发脚手架. 适合个人开发者的小型项目或者公司内部 ...