趣头条面试题:ThreadLocal是什么?怎么用?为什么用它?有什么缺点
作者:Sicimike
blog.csdn.net/Baisitao_/article/details/100063561
前言
相信很多同学都听过ThreadLocal,即使没用过也听过。但是要仔细一问ThreadLocal是个啥,很多同学也不一定能说清楚。本篇博客就是为了回答关于ThreadLocal的一系列灵魂拷问:ThreadLocal是个什么?怎么用?为什么要用它?它有什么缺点?怎么避免…
ThreadLoacl是什么
在了解ThreadLocal之前,我们先了解下什么是线程封闭
把对象封闭在一个线程里,即使这个对象不是线程安全的,也不会出现并发安全问题。
实现线程封闭大致有三种方式:
Ad-hoc线程封闭:维护线程封闭性的职责完全由程序来承担,不推荐使用
栈封闭:就是用栈(stack)来保证线程安全
public void testThread() {
StringBuilder sb = new StringBuilder();
sb.append("Hello");
}
StringBuilder是线程不安全的,但是它只是个局部变量,局部变量存储在虚拟机栈,虚拟机栈是线程隔离的,所以不会有线程安全问题
ThreadLocal线程封闭:简单易用
第三种方式就是通过ThreadLocal来实现线程封闭,线程封闭的指导思想是封闭,而不是共享。所以说ThreadLocal是用来解决变量共享的并发安全问题,多少有些不精确。
使用
JDK1.2开始提供的java.lang.ThreadLocal的使用方式非常简单
public class ThreadLocalDemo {
public static void main(String[] args) throws InterruptedException {
final ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("main-thread : Hello");
Thread thread = new Thread(() -> {
// 获取不到主线程设置的值,所以为null
System.out.println(threadLocal.get());
threadLocal.set("sub-thread : World");
System.out.println(threadLocal.get());
});
// 启动子线程
thread.start();
// 让子线程先执行完成,再继续执行主线
thread.join();
// 获取到的是主线程设置的值,而不是子线程设置的
System.out.println(threadLocal.get());
threadLocal.remove();
System.out.println(threadLocal.get());
}
}
运行结果
null
sub-thread : World
main-thread : Hello
null
运行结果说明了ThreadLocal只能获取本线程设置的值,也就是线程封闭。基本上,ThreadLocal对外提供的方法只有三个get()、set(T)、remove()。
原理
使用方式非常简单,所以我们来看看ThreadLocal的源码。ThreadLocal内部定义了一个静态ThreadLocalMap类,ThreadLocalMap内部又定义了一个Entry类,这里只看一些主要的属性和方法
public class ThreadLocal<T> {
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();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
// 从这里可以看出ThreadLocalMap对象是被Thread类持有的
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// 内部类ThreadLocalMap
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
// 内部类Entity,实际存储数据的地方
// Entry的key是ThreadLocal对象,不是当前线程ID或者名称
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 注意这里维护的是Entry数组
private Entry[] table;
}
}
根据上面的源码,可以大致画出ThreadLocal在虚拟机内存中的结构
实线箭头表示强引用,虚线箭头表示弱引用(关于对象的四种引用,可以参考博主之前的博客:Java中四种引用)。需要注意的是:
ThreadLocalMap虽然是在ThreadLocal类中定义的,但是实际上被Thread持有。 Entry的key是(虚引用的)ThreadLocal对象,而不是当前线程ID或者线程名称。 ThreadLocalMap中持有的是Entry数组,而不是Entry对象。
对于第一点,ThreadLocalMap被Thread持有是为了实现每个线程都有自己独立的ThreadLocalMap对象,以此为基础,做到线程隔离。第二点和第三点理解,我们先来想一个问题,如果同一个线程中定义了多个ThreadLocal对象,内存结构应该是怎样的?此时再来看一下ThreadLocal.set(T)方法:
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 根据线程对象获取ThreadLocalMap对象(ThreadLocalMap被Thread持有)
ThreadLocalMap map = getMap(t);
// 如果ThreadLocalMap存在,则直接插入;不存在,则新建ThreadLocalMap
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
也就是说,如果程序定义了多个ThreadLocal,会共用一个ThreadLocalMap对象,所以内存结构应该是这样
这个内存结构图解释了第二点和第三点。假设Entry中key为当前线程ID或者名称的话,那么程序中定义多个ThreadLocal对象时,Entry数组中的所有Entry的key都一样(或者说只能存一个value)。ThreadLocalMap中持有的是Entry数组,而不是Entry,则是因为程序可定义多个ThreadLocal对象,自然需要一个数组。
内存泄漏
ThreadLocal会发生内存泄漏吗?
会
仔细看下ThreadLocal内存结构就会发现,Entry数组对象通过ThreadLocalMap最终被Thread持有,并且是强引用。也就是说Entry数组对象的生命周期和当前线程一样。即使ThreadLocal对象被回收了,Entry数组对象也不一定被回收,这样就有可能发生内存泄漏。ThreadLocal在设计的时候就提供了一些补救措施:
Entry的key是弱引用的ThreadLocal对象,很容易被回收,导致key为null(但是value不为null)。所以在调用get()、set(T)、remove()等方法的时候,会自动清理key为null的Entity。 remove()方法就是用来清理无用对象,防止内存泄漏的。所以每次用完ThreadLocal后需要手动remove()。
有些文章认为是弱引用导致了内存泄漏,其实是不对的。假设把弱引用变成强引用,这样无用的对象key和value都不为null,反而不利于GC,只能通过remove()方法手动清理,或者等待线程结束生命周期。也就是说ThreadLocalMap的生命周期由持有它的线程来决定,线程如果不进入terminated状态,ThreadLocalMap就不会被GC回收,这才是ThreadLocal内存泄露的原因。
应用场景
维护JDBC的java.sql.Connection对象,因为每个线程都需要保持特定的Connection对象。 Web开发时,有些信息需要从controller传到service传到dao,甚至传到util类。看起来非常不优雅,这时便可以使用ThreadLocal来优雅的实现。 包括线程不安全的工具类,比如Random、SimpleDateFormat等
与synchronized的关系
有些文章拿ThreadLocal和synchronized比较,其实它们的实现思想不一样。
synchronized是同一时间最多只有一个线程执行,所以变量只需要存一份,算是一种时间换空间的思想 ThreadLocal是多个线程互不影响,所以每个线程存一份变量,算是一种空间换时间的思想
总结
ThreadLocal是一种隔离的思想,当一个变量需要进行线程隔离时,就可以考虑使用ThreadLocal来优雅的实现。
Springboot启动扩展点超详细总结,再也不怕面试官问了 List去除重复数据的五种方式,学到了... SpringBoot操作ES进行各种高级查询(必须收藏) 10w行级别数据的Excel导入优化记录 再见,HttpClient!再见,Okhttp! 使用Docker部署SpringBoot+Vue系统 快试试Java8中的StringJoiner吧,真香!