王有志,一个分享硬核Java技术的互金摸鱼侠

加入Java人的提桶跑路群:共同富裕的Java人

今天是《面霸的自我修养》第5篇文章,我们一起来看看面试中会问到哪些关于ThreadLocal的问题吧。
数据来源:

  • 大部分来自于各机构(Java之父,Java继父,某灵,某泡,某客)以及各博主整理文档;
  • 小部分来自于我以及身边朋友的实际经理,题目上会做出标识,并注明面试公司。

叠“BUFF”:

  • 八股文通常出现在面试的第一二轮,是“敲门砖”,但仅仅掌握八股文并不能帮助你拿下Offer;
  • 由于本人水平有限,文中难免出现错误,还请大家以批评指正为主,尽量不要喷~~
  • 本文及历史文章已经完成PDF文档的制作,提取关键字【面霸的自我修养】。

ThreadLocal是什么?它有什么作用?

难易程度

重要程度

面试公司:腾讯

先来看Java源码中的ThreadLocal的注释:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable.

ThreadLocal提供了线程的局部变量。线程局部变量与普通变量不同之处在于,访问线程局部变量的每个线程(通过get方法或set方法)都有它自己的独立初始化的变量副本
ThreadLocal“属于”线程,用于存储与线程相关的变量,以保证线程安全。需要注意的是,注释中的“copy of the variable”会让你认为是ThreadLocal内部实现了对存储变量的拷贝,实际上并不是这样的,
简单来说,ThreadLocal用于存储只与线程相关的变量。需要注意的是,ThreadLocal本身并不会拷贝变量,如果使用不当仍旧存在线程安全问题。这点可以在下面ThreadLocal的实现原理中看到,或者可以阅读我之前写的《ThreadLocal的那点小秘密》。


描述ThreadLocal的实现原理。

难易程度

重要程度

面试公司:百度,腾讯,海康威视

先来看ThreadLocal怎么使用,以最常见的ThreadLocal存储线程独立的SimpleDateFormat为例:

private static final ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<>(){
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
}; public static void main(String[] args) {
SimpleDateFormat simpleDateFormat = threadLocal.get();
System.out.println(simpleDateFormat.format(new Date()));
threadLocal.set(new SimpleDateFormat(""));
}

众所周知,SimpleDateFormat存在并发安全问题,通常建议通过ThreadLocal来使用SimpleDateFormat,以避免并发安全问题,那么ThreadLocal能解决并发安全问题的原理是什么呢?
我们通过ThreadLocal的源码来一步一步的分析,先来看ThreadLocal的构造器和ThreadLocal#initialValue方法:

public ThreadLocal() {
} protected T initialValue() {
return null;
}

两个方法均没有做任何实现,但我们在创建ThreadLocal时重写了ThreadLocal#initialValue方法,不过目前还没有看到在哪里调用,我们接着往下看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();
}

整体看一下ThreadLocal#get方法的实现:

  • 第2行代码,获取到当前线程;
  • 第3行代码,通过当前线程获取到ThreadLocalMap;
  • 第4~11行代码,通过ThreadLocal实例(this对象)查询并返回ThreadLocalMap中的结果结果;
  • 第12行代码,通过名字可以猜测到是设置初始值的方法。

第2行代码并没有什么可以解释的,来看第3行代码中的ThreadLocal#getMap方法实现:

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

ThreadLocal#getMap方法返回的是Thread的变量threadLocals,再来看Thread中是如何定义threadLocals的:

ThreadLocal.ThreadLocalMap threadLocals = null;

可以看到Thread使用了ThreadLocal的内部类ThreadLocalMap,这里我们先通过源码中的注释来了解ThreadLocalMap是什么。

ThreadLocalMap is a customized hash map suitable only for maintaining thread local values. No operations are exported outside of the ThreadLocal class.

提取这段话的关键点:

  • “a customized hash map”,定制化的散列表,解释了ThreadLocalMap的本质;
  • “No operations are exported outside of the ThreadLocal class”,只有在ThreadLocal内部会调用ThreadLocalMap的操作,解释了ThreadLocalMap作为ThreadLocal的内部类的原因。

为了便于后面的理解,可以把ThreadLocalMap直接看做是HashMap,否则可能会陷入Thread,ThreadLocal和ThreadLocalMap的“混乱”关系中。
接着回到ThreadLocal#get方法的代码中,假设通过ThreadLocal#getMap方法获取到了ThreadLocalMap,则进入第4行的if语句中,通过ThreadLocal对象(ThreadLocalMap的key)获取结果(ThreadLocalMap的value)。
至此,我们已经能够建立Thread,ThreadLocal和ThreadLocalMap的关系:



这个关系中,ThreadLocal是如何帮助线程实现变量的线程隔离的呢?实际上就是Thread内部维护了一个Map用于存储线程间独立的变量,而ThreadLocal是作为Map的Key。要知道Thread的实例就是Java里的线程,Thread实例独有的,就是线程独有的,这点我在《关于线程你必须知道的8个问题(上)》中也提到过。
接着回到ThreadLocal#get方法中,来看第12行调用的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方法中,第2行就是调用ThreadLocal#initialValue,实际上调用的是在使用ThreadLocal时重写的部分。后面的第49行代码是获取或创建ThreadLocalMap,第1012行是是ThreadLocal的子类TerminatingThreadLocal相关的内容,这里我们也不过多深入。
现在来思考下,如果在实现ThreadLocal#initialValue方法时提供了一个共享变量会发生什么?例如:

private static Boolean flag = false;

private static final ThreadLocal<Boolean> threadLocal = new ThreadLocal<>() {
@Override
protected Boolean initialValue() {
return flag;
}
}; public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
flag = true;
System.out.println("线程1:结束");
}).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> {
while (threadLocal.get()) {
System.out.println("线程2:死循环");
}
System.out.println("线程2:结束");
}).start();
}

上面的代码不难理解:

  • 第1行创建了标记flag;
  • 第3~8行创建了ThreadLocal的实例,并存储了flag;
  • 第11~14行中,线程1修改了标记flag;
  • 第18~23行中,线程2根据标记决定是否进入循环。

执行上面代码会发现线程2进入了死循环,好像并没有起到线程隔离的作用,这是为什么?
答案也并不复杂,实际上就是通过ThreadLoca存入到ThreadLocalMap的变量并不是线程独立的,也就是说,只有变量本身是线程独立的,ThreadLocal才能起到变量线程隔离的作用。
说到这单的原因是,有些文章的描述可能会让大家认为是ThreadLocal内部实现了变量的拷贝,但实际情况是要先有变量的拷贝并存储ThreadLocal,而不是存入ThreadLocal的变量会“自动”拷贝出一份副本
按照以上分析的内容,可以看到ThreadLocal的本质并不能解决并发安全问题,它的主要功能应该是在线程内传递变量

ThreadLocal会造成内存泄漏吗?

难易程度

重要程度

面试公司:百度,美团

ThreadLocal是存在内存泄漏的风险的,或者确切的说ThreadLocalMap存在内存泄漏的风险,我们来看ThreadLocalMap的源码:

static class ThreadLocalMap {

	static class Entry extends WeakReference<ThreadLocal<?>> {

		Object value;

		Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
} private Entry[] table; 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的内部类Entry是继承自WeakReference,在Entry的构造方法中,作为Key的ThreadLocal被设置为弱引用。
那么存就在一种情况,如果ThreadLocal自身失去外部的强引用且在发生GC后,Entry中作为key的ThreadLocal被回收,但value与Entry间存在强引用关系,且ThreadLocalMap是Thread的成员变量,那么Thread未被销毁时,ThreadLocalMap中会存在一个key为null,但value依旧存在Entry,而它会占用内存空间,却无法正常访问,此时会造成内存泄漏
来看一段代码:

public static void main(String[] args) throws InterruptedException {
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "Hello");
System.out.println(threadLocal.get());
Thread t = Thread.currentThread();
threadLocal = null;
System.gc();
System.out.println(t.getName());
}

代码本身没有任何含义,只是为了展示ThreadLocal内存泄漏的场景。其中第4~5行代码,主动将threadLocal指向null,并且调用System#gc进行GC,这里我们通过Debug前后的代码来印证。
发生GC前,我们来观察Thread#threadLocals中的存储情况:



可以看到主线程的ThreadLocalMap中已经存入字符串“Hello”,并且Key是我们声明的ThreadLocal变量,此时一切正常。
接着来看发生GC后Thread#threadLocals中的存储情况:



可以看到ThreadLocalMap中,字符串“Hello”对应的引用已经为null,无法正常访问到,就造成了内存泄漏。
如果线程的生命周期较短,使用完成后就立即销毁,那么内存泄漏的危害并不严重,可如果是通过线程池创建的线程,使用完成后并不会立即销毁,那么内存泄漏的问题就会持续存在。

如何避免ThreadLocal的内存泄漏?

难易程度

重要程度

面试公司:无

通常我们不会写主动将threadLocal指向null,如果真的需要这么做的话,首先应该先调用ThreadLocal#remove方法,来删除ThreadLocal中存储的数据。该方法源码如下;

public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}

除了主动调用ThreadLocal#remove外,可以使用static修饰ThreadLocal的实例,保证ThreadLocal的强引用一直存在,这是也Java推荐的做法,源码中对此有说明:

ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

当然,如果存在过多的使用static修饰,但无用的ThreadLocal实例,可能会造成内存溢出的问题,这点也是我们在代码中需要考虑的部分。

参考资料


如果本文对你有帮助的话,还请多多点赞支持。如果文章中出现任何错误,还请批评指正。最后欢迎大家关注分享硬核Java技术的金融摸鱼侠王有志,我们下次再见!

面霸的自我修养:ThreadLocal专题的更多相关文章

  1. GIS制图人员的自我修养(1)--制图误区

    GIS制图人员的自我修养 by 李远祥 最近一直坚持写GIS制图的技术专题,并不是为了要介绍有什么好的技术和方法去制图,而是要告诉所有从事这一方向的人员一个铁铮铮的实现--要做好GIS制图,必须加强自 ...

  2. IT技术管理者的自我修养

    1. 前言 本来写<IT技术管理者的自我修养>与<IT技术人员的自我修养>是一开始就有的想法.但发表<IT技术人员的自我修养>后,收到了不少良好的反馈,博客园的编辑 ...

  3. 《web全栈工程师的自我修养》读书笔记

    有幸读了yuguo<web全栈工程师的自我修养>,颇有收获,故在此对读到的内容加以整理,方便指导,同时再回顾一遍书中的内容. 概览 整本书叙述的是作者的成长经历,通过经验的分享,给新人或者 ...

  4. 程序员的自我修养(2)——计算机网络(转) good

    相关文章:程序员的自我修养——操作系统篇 几乎所有的计算机程序,都会牵涉到网络通信.因此,了解计算机基础网络知识,对每一个程序员来说都是异常重要的. 本文在介绍一些基础网络知识的同时,给出了一些高质量 ...

  5. GIS制图人员的自我修养(2)--制图意识

    GIS制图人员的自我修养(2)--制图意识 by 李远祥 上次提及到GIS制图人员的一些制图误区,主要是为GIS制图人员剖析在制图工作中的一些问题.但如何提高制图的自我修养,却是一个非常漫长的过程,这 ...

  6. web性能优化 来自《web全栈工程师的自我修养》

    最近在看<web全栈工程师的自我修养>一书,作者是来自腾讯的前端工程师.作者在做招聘前端的时候问应聘者web新能优化有什么了解和经验,应聘者思索后回答“在发布项目之前压缩css和 Java ...

  7. gcc ld 链接器相关知识,调试指令(程序员的自我修养----链接、装载与库)

    最近解决一个动态链接上的问题,因为以前从来没有接触过这方面的知识,所以恶补了一下,首先要了解gcc编译指令(makefile),ld链接器的选项(还有连接脚本section指定内存位置),熟悉查看连接 ...

  8. Python学习笔记(四十九)爬虫的自我修养(一)

    论一只爬虫的自我修养 URL的一般格式(带括号[]的为可选项): protocol://hostname[:port]/path/[;parameters][?query]#fragment URL由 ...

  9. Hacker的社交礼仪与自我修养【转】

    Hacker School是位于纽约的一所特殊的编程“学校”,他们的目标是帮助参与者变成“更好的程序员”,之所以说他们特殊是因为这所“学校”没有老师,没有考试,也不会颁发证书,他们信奉三人行必有我师, ...

  10. 第八周读书笔记(人月神话X月亮与六便士)——到底什么才是一个程序员的自我修养?

    写了这么久的读书笔记,涉及到问题大多是一些如何把软件工程做好,如何把自己的职业生涯做好.但总感觉逻辑链上缺了一环,亦即:我们为什么要把软件工程做好,我们成为一名优秀的职业生涯的意义到底在于什么?我觉得 ...

随机推荐

  1. 【python基础】编写/运行hello world项目

    1.编写hello world项目 编程界每种语言的第一个程序往往都是输出hello world.因此我们来看看,如何用Python输出hello world. 1.如果你是初学者,main.py中的 ...

  2. 微生物组分析软件 QIIME 2 安装小记

    由于微信不允许外部链接,你需要点击文章尾部左下角的 "阅读原文",才能访问文中链接. QIIME 2 是一个功能强大,可扩展,分散式的(decentralized)微生物组分析软件 ...

  3. http_basic认证(401)爆破

    Http Basic认证(401)爆破 hydra,burpsuit 在thm:https://tryhackme.com/room/toolsrus 遇到了这个问题,但这个用的工具是hydra,想起 ...

  4. 网站开发[1] - Spring Boot 快速建立项目

    前言 学校的数据库课程要求做出前端页面对数据库进行交互, 可以使用 Python 或者 Java 语言作为后端, Python语言使用起来非常方便, 但出于对自己的挑战以及更加贴合实际企业开发, 我选 ...

  5. [MAUI]写一个跨平台富文本编辑器

    @ 目录 原理 创建编辑器 定义 实现复合样式 选择范围 字号 字体颜色与背景色 字体下划线 字体加粗与斜体 序列化和反序列化 跨平台实现 集成至编辑器 创建控件 使用控件 最终效果 已知问题 项目地 ...

  6. @Repeatable元注解的使用

    @Repeatable注解表明标记的注解可以多次应用于相同的声明或类型,此注解由Java SE 8版本引入.以下示例如何使用此注解: 第一步,先声明一个重复注解类: package org.sprin ...

  7. macOS 系统 Kafka 快速入门

    Kafka 的核心功能是高性能的消息发送与高性能的消息消费.以下是 Kafka 的快速入门教程. 下载并解压缩 Kafka 二进制代码压缩文件 打开 Kafka 官网的下载地址,可以看到不同版本的 K ...

  8. 数据结构课后题答案 - XDU_953

    参考书: 数据结构与算法分析(第二版) 作者:荣政 编 出版社:西安电子科技大学出版社 出版日期:2021年01月01日 答案解析:

  9. Linux网络设备命名规则简介

    Linux网络设备命名规则简介 几年前, Linux内核为网络接口分配名称采用的是一种简单和直观的方式:一个固定的前缀和一个递增的序号.比如,内核使用eth0名称以标识启动后第一个加载的网络设备,第二 ...

  10. JVM GC配置指南

    本文旨在简明扼要说明各回收器调优参数,如有疏漏欢迎指正. 1.JDK版本 以下所有优化全部基于JDK8版本,强烈建议低版本升级到JDK8,并尽可能使用update_191以后版本. 2.如何选择垃圾回 ...