面试再问ThreadLocal,别说你不会!

ThreadLocal是什么
以前面试的时候问到ThreadLocal总是一脸懵逼,只知道有这个哥们,不了解他是用来做什么的,更不清楚他的原理了。表面上看他是和多线程,线程同步有关的一个工具类,但其实他与线程同步机制无关。
线程同步机制是多个线程共享同一个变量,而ThreadLocal是为每个线程创建一个单独的变量副本,每个线程都可以改变自己的变量副本而不影响其它线程所对应的副本。
官方API上是这样介绍的:该类提供了线程局部(thread-local)变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
ThreadLocal的API
ThreadLocal定义了四个方法:
get():返回此线程局部变量当前副本中的值
set(T value):将线程局部变量当前副本中的值设置为指定值
initialValue():返回此线程局部变量当前副本中的初始值
remove():移除此线程局部变量当前副本中的值
ThreadLocal还有一个特别重要的静态内部类ThreadLocalMap,该类才是实现线程隔离机制的关键。get()、set()、remove()都是基于该内部类进行操作,ThreadLocalMap用键值对方式存储每个线程变量的副本,key为当前的ThreadLocal对象,value为对应线程的变量副本。
试想,每个线程都有自己的ThreadLocal对象,也就是都有自己的ThreadLocalMap,对自己的ThreadLocalMap操作,当然是互不影响的了,这就不存在线程安全问题了,所以ThreadLocal是以空间来交换安全性的解决思路。
使用实例
假设每个线程都需要一个计数值记录自己做某件事做了多少次,各线程运行时都需要改变自己的计数值而且相互不影响,那么ThreadLocal就是很好的选择,这里ThreadLocal里保存的当前线程的局部变量的副本就是这个计数值。
public class SeqCount {
private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public int nextSeq() {
seqCount.set(seqCount.get() +1);
return seqCount.get();
}
public static void main(String [] args) {
SeqCount seqCount = new SeqCount();
SeqThread seqThread1 = new SeqThread(seqCount);
SeqThread seqThread2 = new SeqThread(seqCount);
SeqThread seqThread3 = new SeqThread(seqCount);
SeqThread seqThread4 = new SeqThread(seqCount);
seqThread1.start();
seqThread2.start();
seqThread3.start();
seqThread4.start();
}
public static class SeqThread extends Thread {
private SeqCount seqCount;
public SeqThread(SeqCount seqCount) {
this.seqCount = seqCount;
}
@Override
public void run() {
for (int i=0; i<3; i++) {
System.out.println(Thread.currentThread().getName()+" seqCount:"+seqCount.nextSeq());
}
}
}
}
运行结果:

解决SimpleDateFormat的线程安全
我们知道SimpleDateFormat在多线程下是存在线程安全问题的,那么将SimpleDateFormat作为每个线程的局部变量的副本就是每个线程都拥有自己的SimpleDateFormat,就不存在线程安全问题了。
public class SimpleDateFormatDemo {
private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<>();
/**
* 获取线程的变量副本,如果不覆盖initialValue方法,第一次get将返回null,故需要创建一个DateFormat,放入threadLocal中
* @return
*/
public DateFormat getDateFormat() {
DateFormat df = threadLocal.get();
if (df == null) {
df = new SimpleDateFormat(DATE_FORMAT);
threadLocal.set(df);
}
return df;
}
public static void main(String [] args) {
SimpleDateFormatDemo formatDemo = new SimpleDateFormatDemo();
MyRunnable myRunnable1 = new MyRunnable(formatDemo);
MyRunnable myRunnable2 = new MyRunnable(formatDemo);
MyRunnable myRunnable3 = new MyRunnable(formatDemo);
Thread thread1= new Thread(myRunnable1);
Thread thread2= new Thread(myRunnable2);
Thread thread3= new Thread(myRunnable3);
thread1.start();
thread2.start();
thread3.start();
}
public static class MyRunnable implements Runnable {
private SimpleDateFormatDemo dateFormatDemo;
public MyRunnable(SimpleDateFormatDemo dateFormatDemo) {
this.dateFormatDemo = dateFormatDemo;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 当前时间:"+dateFormatDemo.getDateFormat().format(new Date()));
}
}
}
运行结果:

源码分析
ThreadLocalMap
ThreadLocalMap内部是利用Entry来进行key-value的存储的。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
上面源码中key就是ThreadLocal,value就是值,Entry继承WeakReference,所以Entry对应key的引用(ThreadLocal实例)是一个弱引用。
set(ThreadLocal key, Object value)
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//根据ThreadLocal的散列值,查找对应元素在数组中的位置
int i = key.threadLocalHashCode & (len-1);
//采用线性探测法寻找合适位置
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//key存在,直接覆盖
if (k == key) {
e.value = value;
return;
}
// key == null,但是存在值(因为此处的e != null),说明之前的ThreadLocal对象已经被回收了
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//ThreadLocal对应的key实例不存在,new一个
tab[i] = new Entry(key, value);
int sz = ++size;
//清楚陈旧的Entry(key == null的)
// 如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值,则进行 rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
这个set操作和集合Map解决散列冲突的方法不同,集合Map采用的是链地址法,这里采用的是开放定址法(线性探测)。set()方法中的replaceStaleEntry()和cleanSomeSlots(),这两个方法可以清除掉key ==null的实例,防止内存泄漏。
getEntry()
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
由于采用了开放定址法,当前key的散列值和元素在数组中的索引并不是一一对应的,首先取一个猜测数(key的散列值),如果所对应的key是我们要找的元素,那么直接返回,否则调用getEntryAfterMiss()
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
这里一直在探测寻找下一个元素,知道找的元素的key是我们要找的。这里当key==null时,调用expungeStaleEntry有利于GC的回收,用于防止内存泄漏。
ThreadLocal为什么会内存泄漏
ThreadLocalMap的key为ThreadLocal实例,他是一个弱引用,我们知道弱引用有利于GC的回收,当key == null时,GC就会回收这部分空间,但value不一定能被回收,因为他和Current Thread之间还存在一个强引用的关系。由于这个强引用的关系,会导致value无法回收,如果线程对象不消除这个强引用的关系,就可能会出现OOM。有些时候,我们调用ThreadLocalMap的remove()方法进行显式处理。

总结
ThreadLocal不是用来解决共享变量的问题,也不是协调线程同步,他是为了方便各线程管理自己的状态而引用的一个机制。每个ThreadLocal内部都有一个ThreadLocalMap,他保存的key是ThreadLocal的实例,他的值是当前线程的局部变量的副本的值。
public class SimpleDateFormatDemo {
private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<>();
/**
* 获取线程的变量副本,如果不覆盖initialValue方法,第一次get将返回null,故需要创建一个DateFormat,放入threadLocal中
* @return
*/
public DateFormat getDateFormat() {
DateFormat df = threadLocal.get();
if (df == null) {
df = new SimpleDateFormat(DATE_FORMAT);
threadLocal.set(df);
}
return df;
}
public static void main(String [] args) {
SimpleDateFormatDemo formatDemo = new SimpleDateFormatDemo();
MyRunnable myRunnable1 = new MyRunnable(formatDemo);
MyRunnable myRunnable2 = new MyRunnable(formatDemo);
MyRunnable myRunnable3 = new MyRunnable(formatDemo);
Thread thread1= new Thread(myRunnable1);
Thread thread2= new Thread(myRunnable2);
Thread thread3= new Thread(myRunnable3);
thread1.start();
thread2.start();
thread3.start();
}
public static class MyRunnable implements Runnable {
private SimpleDateFormatDemo dateFormatDemo;
public MyRunnable(SimpleDateFormatDemo dateFormatDemo) {
this.dateFormatDemo = dateFormatDemo;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 当前时间:"+dateFormatDemo.getDateFormat().format(new Date()));
}
}
}
面试再问ThreadLocal,别说你不会!的更多相关文章
- 面试再问ThreadLocal,别说你不会
转载自:公众号<Java知音> ThreadLocal是什么 以前面试的时候问到ThreadLocal总是一脸懵逼,只知道有这个哥们,不了解他是用来做什么的,更不清楚他的原理了.表面上看他 ...
- Java面试必问-ThreadLocal
前言 在面试环节中,考察"ThreadLocal"也是面试官的家常便饭,所以对它理解透彻,是非常有必要的. 有些面试官会开门见山的提问: “知道ThreadLocal吗?” “讲讲 ...
- 面试被问烂的 Spring IOC(求求你别再问了)
广义的 IOC IoC(Inversion of Control) 控制反转,即"不用打电话过来,我们会打给你". 两种实现: 依赖查找(DL)和依赖注入(DI). IOC 和 D ...
- 拜托!面试请不要再问我Spring Cloud底层原理[z]
[z]https://juejin.im/post/5be13b83f265da6116393fc7 拜托!面试请不要再问我Spring Cloud底层原理 欢迎关注微信公众号:石杉的架构笔记(id: ...
- 面试官,不要再问我“Java GC垃圾回收机制”了
Java GC垃圾回收几乎是面试必问的JVM问题之一,本篇文章带领大家了解Java GC的底层原理,图文并茂,突破学习及面试瓶颈. 楔子-JVM内存结构补充 在上篇<JVM之内存结构详解> ...
- 面试官,不要再问我“Java 垃圾收集器”了
如果Java虚拟机中标记清除算法.标记整理算法.复制算法.分代算法这些属于GC收集算法中的方法论,那么"GC收集器"则是这些方法论的具体实现. 在面试过程中这个深度的问题涉及的比较 ...
- 面试官,不要再问我“Java虚拟机类加载机制”了
关于Java虚拟机类加载机制往往有两方面的面试题:根据程序判断输出结果和讲讲虚拟机类加载机制的流程.其实这两类题本质上都是考察面试者对Java虚拟机类加载机制的了解. 面试题试水 现在有这样一道判断程 ...
- 面试官,不要再问我“Java虚拟机类加载机制”了(转载)
关于Java虚拟机类加载机制往往有两方面的 面试题:根据程序判断输出结果和讲讲虚拟机类加载机制的流程.其实这两类题本质上都是考察面试者对Java虚拟机类加载机制的了解. 面试题试水 现在有这样一道判断 ...
- 面试官,不要再问我“Java 垃圾收集器”了(转载)
如果Java虚拟机中标记清除算法.标记整理算法.复制算法.分代算法这些属于GC收集算法中的方法论,那么"GC收集器"则是这些方法论的具体实现. 在 面试过程中这个深度的问题涉及的比 ...
随机推荐
- Mac 解决IDEA无法启动的问题
今天重装了IDEA,发现IDEA启动不了,点击IDEA无反应,使用命令行启动idea,然后看看具体的错误,具体的启动方案就是:打开finder->应用程序->idea.app->右键 ...
- SpringCloud与微服务Ⅵ --- Ribbon负载均衡
一.Ribbon是什么 Sping Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具. 简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户 ...
- 【阿里云IoT+YF3300】12.阿里云IoT Studio入门介绍
阿里云IoT Studio是针对物联网场景提供的生产力工具,可覆盖各个物联网行业核心应用场景,帮助您高效经济地完成设备.服务及应用开发.物联网开发服务提供了移动可视化开发.Web可视化开发.服务开发与 ...
- 在Linux环境下设置 ora-01031:insufficient privileges解决方法总结
今天需要使用sys用户处理问题,但是报错上面ora-01031:insufficient privileges. 在网上有很多方法,这个是自己经过测试的方法步骤. 1:首先检查文件sqlnet.ora ...
- CUDA学习(六)之使用共享内存(shared memory)进行归约求和(M个包含N个线程的线程块)
在https://www.cnblogs.com/xiaoxiaoyibu/p/11402607.html中介绍了使用一个包含N个线程的线程块和共享内存进行数组归约求和, 基本思路: 定义M个包含N个 ...
- CUDA学习(四)之使用全局内存进行归约求和(一个包含N个线程的线程块)
问题:使用CUDA进行数组元素归约求和,归约求和的思想是每次循环取半. 详细过程如下: 假设有一个包含8个元素的数组,索引下标从0到7,现通过3次循环相加得到这8个元素的和,使用一个间隔变量,该间隔变 ...
- 实现当前目录下开启CMD
我们都知道在WIN7下,可以按shift+鼠标右键->在此处打开命令窗口 或者在 输入cmd,回车 那么,怎样去实现这样的功能呢? //当前目录下开启CMD #include <stdio ...
- Sophus库CMakeLists.txt内容详解笔记
CMakeLists.txt: SET(PROJECT_NAME Sophus) PROJECT(${PROJECT_NAME}) CMAKE_MINIMUM_REQUIRED(VERSION 2.6 ...
- vue项目实战经验汇总
目录 1.vue框架使用注意事项和经验 1.1 解决Vue动态路由参数变化,页面数据不更新 1.2 vue组件里定时器销毁问题 1.3 vue实现按需加载组件的两种方式 1.4 组件之间,父子组件之间 ...
- 对特殊方法的访问 - Special method lookup
对特殊方法的访问 - Special method lookup 对于用户自定义的 class 来说, 特殊方法只有通过定义对象的类型object’s type (而非通过 instance 的 __ ...