面霸的自我修养:ThreadLocal专题
王有志,一个分享硬核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专题的更多相关文章
- GIS制图人员的自我修养(1)--制图误区
GIS制图人员的自我修养 by 李远祥 最近一直坚持写GIS制图的技术专题,并不是为了要介绍有什么好的技术和方法去制图,而是要告诉所有从事这一方向的人员一个铁铮铮的实现--要做好GIS制图,必须加强自 ...
- IT技术管理者的自我修养
1. 前言 本来写<IT技术管理者的自我修养>与<IT技术人员的自我修养>是一开始就有的想法.但发表<IT技术人员的自我修养>后,收到了不少良好的反馈,博客园的编辑 ...
- 《web全栈工程师的自我修养》读书笔记
有幸读了yuguo<web全栈工程师的自我修养>,颇有收获,故在此对读到的内容加以整理,方便指导,同时再回顾一遍书中的内容. 概览 整本书叙述的是作者的成长经历,通过经验的分享,给新人或者 ...
- 程序员的自我修养(2)——计算机网络(转) good
相关文章:程序员的自我修养——操作系统篇 几乎所有的计算机程序,都会牵涉到网络通信.因此,了解计算机基础网络知识,对每一个程序员来说都是异常重要的. 本文在介绍一些基础网络知识的同时,给出了一些高质量 ...
- GIS制图人员的自我修养(2)--制图意识
GIS制图人员的自我修养(2)--制图意识 by 李远祥 上次提及到GIS制图人员的一些制图误区,主要是为GIS制图人员剖析在制图工作中的一些问题.但如何提高制图的自我修养,却是一个非常漫长的过程,这 ...
- web性能优化 来自《web全栈工程师的自我修养》
最近在看<web全栈工程师的自我修养>一书,作者是来自腾讯的前端工程师.作者在做招聘前端的时候问应聘者web新能优化有什么了解和经验,应聘者思索后回答“在发布项目之前压缩css和 Java ...
- gcc ld 链接器相关知识,调试指令(程序员的自我修养----链接、装载与库)
最近解决一个动态链接上的问题,因为以前从来没有接触过这方面的知识,所以恶补了一下,首先要了解gcc编译指令(makefile),ld链接器的选项(还有连接脚本section指定内存位置),熟悉查看连接 ...
- Python学习笔记(四十九)爬虫的自我修养(一)
论一只爬虫的自我修养 URL的一般格式(带括号[]的为可选项): protocol://hostname[:port]/path/[;parameters][?query]#fragment URL由 ...
- Hacker的社交礼仪与自我修养【转】
Hacker School是位于纽约的一所特殊的编程“学校”,他们的目标是帮助参与者变成“更好的程序员”,之所以说他们特殊是因为这所“学校”没有老师,没有考试,也不会颁发证书,他们信奉三人行必有我师, ...
- 第八周读书笔记(人月神话X月亮与六便士)——到底什么才是一个程序员的自我修养?
写了这么久的读书笔记,涉及到问题大多是一些如何把软件工程做好,如何把自己的职业生涯做好.但总感觉逻辑链上缺了一环,亦即:我们为什么要把软件工程做好,我们成为一名优秀的职业生涯的意义到底在于什么?我觉得 ...
随机推荐
- Rust 语言风靡学术界
AWS 将 Rust 编译器团队负责人收入麾下的新闻让开发者们再次聚焦于这门兼具安全性与高性能的编程语言.近日,著名科学期刊 Nature 刊登了一篇文章,表明 Rust 语言也正在成为学术界最受欢迎 ...
- 让优惠再续一年!SHPC 老客专享
最近云筏君经常收到自家小伙伴发来的关于产品活动的关心慰问,掐指一算,哦,原来是一年一度大家喜闻乐见的剁手节(学名"双十一")马上要来了! 大家都知道,云筏家的产品向来主打高性价比, ...
- 【Python爬虫】批量爬取网页的图片&制作数据集
由于Python拥有强大且丰富的类库,语法简单,效率高而被广泛运用于网络爬虫,很多人都是通过爬虫认识Python. 因为小编最近正在做目标识别相关的项目,所以需要大量的 ...
- 开源 API 网关的访问策略(一)
许多企业和组织面临着网关访问控制的挑战,因为传统的访问控制方法往往过于笨重和繁琐.这些方法可能涉及复杂的规则集.繁琐的手动配置过程.缺乏灵活性和可扩展性等问题.此外,随着云计算和移动设备的广泛应用,访 ...
- 采集douban
# -*- coding: utf-8 -*-"""Created on Thu Oct 31 16:14:02 2019 @author: DELL"&quo ...
- Kurator v0.4.0版本更新4大内容,满足多云环境的复杂需求
摘要:在最新发布的 v0.4.0 版本中,Kurator 进一步丰富了分布式云原生场景下的应用统一管理能力,以便更好地满足多云环境的复杂需求. 本文分享自华为云社区<Kurator v0.4.0 ...
- 2020 OC项目集成flutter
看了网上基本上都是相同套路,但是到我这就不行了,终于解决了, 主要就是profile文件编写, 1.在oc项目的同级目录项创建flutter模块 就是在oc项目的父级目录下,执行 flutter cr ...
- AntdPro中formItemProps和fieldProps的区别
最近在工作中接触到了 antd 和 antd pro,作为一个 react 和 antd 新人,在学习和使用中遇到了不少的问题,下边就常见的一个问题来进行记录,后续还会记录更多的问题以及心得 Form ...
- Centos安装ELK
目录 安装Elastic Search 安装 Java 安装Elastic Search 修改配置 开放端口 访问地址 相关命令 安装elasticsearch-head插件 安装Git 安装node ...
- CF1829H Don't Blame Me题解
题意: 给定一个长度为 \(n\) 的数组,选择它的一个子序列(不一定要连续的),问有多少种选法使得它们 AND 的值的二进制表示法中有 \(k\) 个 \(1\). 思路: 这个题就是一个简单的 D ...