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

好久不见,不知道大家新年过得怎么样?有没有痛痛快快得放松?是不是还能收到很多压岁钱?好了,话不多说,我们开始今天的主题: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. ES6学习笔记(七)正则表达式

    正则表达式 1.基础 1.1 含义: 通俗的来讲,正则表达式是一种匹配和替换的工具.如:在JS中验证手机号时,我们需要考虑用户输入的字符必须是number类型,且必须是11位的整数,且数字的前三位必须 ...

  2. Burpsuite(科学版)安装教程

    前言 BurpSuite是一款用于攻击web 应用程序的集成平台,在安全圈被称作"抓包神器".本文主要讲解 BurpSuite破解版的安装教程. 配置环境变量 BurpSuite是 ...

  3. 第2-1-4章 SpringBoot整合FastDFS文件存储服务

    目录 5 SpringBoot整合 5.1 操作步骤 5.2 项目依赖 5.3 客户端开发 5.3.1 FastDFS配置 5.3.2 FastDFS配置类 5.3.3 文件工具类 5.3.4 文件上 ...

  4. js中对小数的计算

    在js 的计算中如果涉及到小数的运算,那结果可不要想当然了,比如  0.1+0.2 的计算 var num1 = 0.1; var num2 = 0.2; console.log(num1+num2) ...

  5. WebApi实现Token验证

    为什么要实现Token呢.在我们客户端发送请求时,如果没有校验数据是否合法那么就有可能造成非法请求泄露我们的数据 实现Token的思路 1.客户端通过用户名和密码来获取Token 通过自己的账号和密码 ...

  6. C++ 之 宏定义

    宏在 C 语言中非常重要,但在 C++ 中却无甚大用,普遍的共识:尽量避免使用宏 C++ 之父 Bjarne 在<C++ Programming Language>中写到 Avoid ma ...

  7. DHorse的链路追踪

    目前,DHorse的链路追踪功能是通过SkyWalking来实现.实现原理是DHorse在部署应用时,通过指定SkyWalking的Agent来收集服务的调用链路信息.下面就来具体看一下DHorse如 ...

  8. 目标检测模型的评价标准-AP与mAP

    目录 目录 目录 前言 一,精确率.召回率与F1 1.1,准确率 1.2,精确率.召回率 1.3,F1 分数 1.4,PR 曲线 1.4.1,如何理解 P-R 曲线 1.5,ROC 曲线与 AUC 面 ...

  9. C/C++标准输入输出函数终极最全解析(不全捶我)

    C/C++的一众输入输出函数的区别常常搞得人晕头转向,二者之中又以输入函数更加令人头疼.本文尝试整理C/C++的各种输入输出函数. 由于输入涉及空格.换行符的读取.忽略等问题,因此输入比输出更麻烦.所 ...

  10. 彻底理解Python中的闭包和装饰器(下)

    上篇讲了Python中的闭包,本篇要讲的装饰器就是闭包的一个重要应用. 如果你还不知道什么是闭包,猛戳这里阅读:彻底理解Python中的闭包和装饰器(上) 什么是装饰器 装饰器的作用是在不修改函数定义 ...