分享是最有效的学习方式。

博客:https://blog.ktdaddy.com/

故事

地铁上,小帅无力地倚靠着杆子,脑子里尽是刚才面试官的夺命连环问,“用过TheadLocal么?ThreadLocal是如何解决共享变量访问的安全性的呢?你觉得啥场景下会用到TheadLocal? 我们在日常用ThreadLocal的时候需要注意什么?ThreadLocal在高并发场景下会造成内存泄漏吗?为什么?如何避免?......”

这些问题,如同阴影一般,在小帅的脑海里挥之不去。

是的,他万万没想到,自诩“多线程小能手”的他栽在了ThreadLocal上。

这是小帅苦投了半个月简历之后才拿到的面试机会,然而又丧失了。当下行情实在是卷到了极点。

都两个月了,面试机会少,居然还每次都被问翻,这样下去真要回老家另谋出路了,小帅内心五味成杂......

小伙伴们,试问一下,如果是你,面对上述的问题,你能否对答如流呢?

概要

既然被问到了,那么作为事后诸葛的老猫就和大家一起来接面试官的招吧。

我们将从以下点来全面剖析一下ThreadLocal。

基本篇

什么是ThreadLocal?

ThreadLocal英文翻译过来就是:线程本地量,它其实是一种线程的隔离机制,保障了多线程环境下对于共享变量访问的安全性。

看到上面的定义之后,那么问题就来了,ThreadLocal是如何解决共享变量访问的安全性的呢?

其实ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。由于副本都归属于各自的线程,所以就不存在多线程共享的问题了。

便于理解,我们看一下下图。

至于上述图中提及的threadLocals(ThreadLocalMap),我们后文看源代码的时候再继续来看。大家心中暂时有个概念。

既然都是保证线程访问的安全性,那么和Synchronized区别是什么呢?

在上面聊到共享变量访问安全性的问题上,其实大家还会很容易想起另外一个关键字Synchronized。聊聊区别吧,整理了一张图,看起来可能会更加直观一些,如下。

通过上图,我们发现ThreadLocal其实是一种线程隔离机制。Synchronized则是一种基于Happens-Before规则里的监视器锁规则从而保证同一个时刻只有一个线程能够对共享变量进行更新。

Synchronized加锁会带来性能上的下降。ThreadLocal采用了空间换时间的设计思想,也就是说每个线程里面都有一个专门的容器来存储共享变量的副本信息,然后每个线程只对自己的变量副本做相对应的更新操作,这样避免了多线程锁竞争的开销。

ThreadLocal的使用

上面说了这么多,咱们来使用一下。就拿SimpleDateFormat来做个例子。当然也会有一道这样的面试题,SimpleDateFormat是否是线程安全的?在阿里Java开发规约中,有强制性的提到SimpleDateFormat 是线程不安全的类。其实主要的原因是由于多线程操作SimpleDateFormat中的Calendar对象引用,然后出现脏读导致的。

踩坑代码:

/**
* @author 公众号:程序员老猫
* @date 2024/2/1 22:58
*/
public class DateFormatTest {
private static final SimpleDateFormat simpleDateFormat =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static Date parse(String dateString) {
Date date = null;
try {
date = simpleDateFormat.parse(dateString);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
} public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(20); for (int i = 0; i < 20; i++) {
executorService.execute(()->{
System.out.println(parse("2024-02-01 23:34:30"));
});
}
executorService.shutdown();
}
}

上述咱们通过线程池的方式针对SimpleDateFormat进行了测试(如果大家需要深入了解一下线程池的相关原理,可以戳“线程池”)。其输出结果如下。

我们可以看到刚开始好好的,后面就异常了。

我们通过ThreadLocal的方式将其优化一下。代码如下:

/**
* @author 公众号:程序员老猫
* @date 2024/2/1 22:58
*/
public class DateFormatTest { private static final ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); public static Date parse(String dateString) {
Date date = null;
try {
date = dateFormatThreadLocal.get().parse(dateString);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
} public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10); for (int i = 0; i < 20; i++) {
executorService.execute(()->{
System.out.println(parse("2024-02-01 23:34:30"));
});
}
executorService.shutdown();
}
}

运行了一下,完全正常了。

Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024

TheadLocal使用场景

那么我们什么时候会用到ThreadLocal呢?

  1. 上面针对SimpleDateFormat的封装也算是一个吧。

  2. 用来替代参数链传递:在编写API接口时,可以将需要传递的参数放入ThreadLocal中,从而不需要在每个调用的方法上都显式地传递这些参数。这种方法虽然不如将参数封装为对象传递来得常见,但在某些情况下可以简化代码结构。

  3. 数据库连接和会话管理:在某些应用中,如Web应用程序,ThreadLocal可以用来保持对数据库连接或会话的管理,以简化并发控制并提高性能。例如,可以使用ThreadLocal来维护一个连接池,使得每个请求都能共享相同的连接,而不是每次都需要重新建立连接。

  4. 全局存储信息:例如在前后端分离的应用中,ThreadLocal可以用来在服务端维护用户的上下文信息或者一些配置信息,而不需要通过HTTP请求携带大量的用户信息。这样做可以在不改变原有架构的情况下,提供更好的用户体验。

如果大家还能想到其他使用的场景也欢迎留言。

升华篇

ThreadLocal原理

上述其实咱们聊的相对而言还是比较浅的。那么接下来,咱们丰富一下之前提到的结构图,从源代码侧深度剖一下ThreadLocal吧。

对应上述图中,解释一下。

  1. 图中有两个线程Thread1以及Thread2。
  2. Thread类中有一个叫做threadLocals的成员变量,它是ThreadLocal.ThreadLocalMap类型的。
  3. ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型对象值。

对应的我们看一下Thread的源代码,如下:

public class Thread implements Runnable {
...
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}

在源码中threadLocals的初始值为Null。

抽丝剥茧,咱们继续看一下ThreadLocalMap在调用构造函数进行初始化的源代码:


static class ThreadLocalMap { private static final int INITIAL_CAPACITY = 16; //初始化容量
private Entry[] table; //ThreadLocalMap数据真正存储在table中
private int size = 0; //ThreadLocalMap条数
private int threshold; // 默认为0,达到这个大小,则扩容
//类Entry的实现
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//构造函数
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY]; //初始化table数组,INITIAL_CAPACITY默认值为16
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //key和16取得哈希值
table[i] = new Entry(firstKey, firstValue);//创建节点,设置key-value
size = 1;
setThreshold(INITIAL_CAPACITY); //设置扩容阈值
}
}

在源码中涉及比较核心的还有set,get以及remove方法。我们依次来看一下:

set方法如下:

 public void set(T value) {
Thread t = Thread.currentThread(); //获取当前线程t
ThreadLocalMap map = getMap(t); //根据当前线程获取到ThreadLocalMap
if (map != null) //如果获取的ThreadLocalMap对象不为空
map.set(this, value); //K,V设置到ThreadLocalMap中
else
createMap(t, value); //创建一个新的ThreadLocalMap
} ThreadLocalMap getMap(Thread t) {
return t.threadLocals; //返回Thread对象的ThreadLocalMap属性
} void createMap(Thread t, T firstValue) { //调用ThreadLocalMap的构造函数
t.threadLocals = new ThreadLocalMap(this, firstValue); //this表示当前类ThreadLocal
}

get方法如下:

    public T get() {
//1、获取当前线程
Thread t = Thread.currentThread();
//2、获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//3、如果map数据不为空,
if (map != null) {
//3.1、获取threalLocalMap中存储的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为null
return 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);
return value;
}

remove方法:

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

那么为什么需要remove方法呢?其实这里会涉及到内存泄漏的问题了。后面咱们细看。

对照着上述的结构图以及源码,如果面试官问ThreadLocal原理的时候,相信大家应该可以说出个所以然来。

  1. Thread线程类有一个类型为ThreadLocal.ThreadLocalMap的变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。
  2. ThreadLocalMap方法内部维护者Entry数组,其中key是ThreadLocal本身,而value则为其泛型值。
  3. 并发场景下,每个线程都会存储当前变量副本到自己的ThreadLocalMap中,后续这个线程对于共享变量的操作,都是从TheadLocalMap里进行变更,不会影响全局共享变量的值。

高并发场景下ThreadLocal会造成内存泄漏吗?什么原因导致?如何避免?

造成内存泄漏的原因

这个问题其实还是得从ThreadLocal底层源码的实现去看。高并发场景下,如果对ThreadLocal处理得当的话其实就不会造成呢村泄漏。我们看下面这样一组源代码片段:

static class ThreadLocalMap {
...
//类Entry的实现
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
}

上文中其实我们已经知道Entry中以key和value的形式存储,key是ThreadLocal本身,上面代码中我们看到entry进行key设置的时候用的是super(k)。那就意味着调用的父类的方法去设置了key,我们再看一下父类是什么,父类其实是WeakReference。关于WeakReference底层的实现,大家有兴趣可以展开去看看源代码,老猫在这里直接说结果。

WeakReference 如字面意思,弱引用,当一个对象仅仅被weak reference(弱引用)指向, 而没有任何其他strong reference(强引用)指向的时候, 如果这时GC运行, 那么这个对象就会被回收,不论当前的内存空间是否足够,这个对象都会被回收。

关于这些引用的强弱,稍微聊一下,这里其实涉及到jvm的回收机制。在JDK1.2之后,java对引用的概念其实做了扩充的,分为强引用,软引用,弱引用,虚引用。

强引用:其实就是咱们一般用“=”的赋值行为,如 Student s = new Student(),只要强引用还在,对象就不会被回收。

软引用:不是必须存活的对象,jvm在内存不够的情况下即将内存溢出前会对其进行回收。例如缓存。

弱引用:非必须存活的对象,引用关系比软引用还弱,无论内存够还是不够,下次的GC一定会被回收。

虚引用:别名幽灵引用或者幻影引用。等同于没有引用,唯一的目的是对象被回收的时候会受到系统通知。

明白这些概念之后,咱们再看看上面的源代码,我们就会发现,原来Key其实是弱引用,而里面的value因为是直接赋值行为所以是强引用。

如下图:

图中我们可以看到由于threadLocal对象是弱引用,如果外部没有强引用指向的话,它就会被GC回收,那么这个时候导致Entry的key就为NULL,如果此时value外部也没有强引用指向的话,那么这个value就永远无法访问了,按道理也该被回收。但是由于entry还在强引用value(看源代码)。那么此时value就无法被回收,此时内存泄漏就出现了。本质原因是因为value成为了一个永远无法被访问也无法被回收的对象。

那肯定有小伙伴会有疑问了,线程本身生命周期不是很短么,如果短时间内被销毁,就不会内存泄漏了,因为只要线程销毁,那么value也会被回收。这话是没错。但是咱们的线程是计算机珍贵资源,为了避免重复创建线程带来开销,系统中我们往往会使用线程池(线程池传送门),如果使用线程池的话,那么线程的生命周期就被拉长了,那么就可想而知了。

如何避免

解法如下:

  1. 每次使用完毕之后记得调用一下remove()方法清除数据。
  2. ThreadLocal变量尽量定义成static final类型,避免频繁创建ThreadLocal实例。这样可以保证程序中一直存在ThreadLocal强引用,也能保证任何时候都能通过ThreadLocal的弱引用访问Entry的value值,从而进行清除。

不过话说出来,其实ThreadLocal内部也做了优化的。在set()的时候也会采样清理,扩容的时候也会检查(这里希望大家自己深入看一下源代码),在get()的时候,如果没有直接命中或者向后环形查找的时候也会进行清理。但是为了系统的稳健万无一失,所以大家尽量还是将上面的两个注意点在写代码的时候注意下。

总结

面试的时候大家总会去背一些八股文,但是这种也只是临时应付面试官而已,真正的懂其中的原理才是硬道理。无论咋问,万变不离核心原理。当然这些核心原理在我们的日常编码中也会给我们带来很大的帮助,用法很简单,翻车了如何处理,那还不是得知其所以然么,伙伴们,你们觉得呢?

服了,一个ThreadLocal被问出了花的更多相关文章

  1. 详解一个ThreadLocal 的谜题

    多线程如果不理解透彻, 那么 ThreadLocal 始终是有些会有所迷糊的. ThreadLocal 本身的命名有有问题, 这些美国精英整出来的技术,再加上一个奇怪的命名.对我们中国人来说,就是一场 ...

  2. 源码篇:ThreadLocal的奇思妙想(万字图文)

    前言 ThreadLocal的文章在网上也有不少,但是看了一些后,理解起来总感觉有绕,而且看了ThreadLocal的源码,无论是线程隔离.类环形数组.弱引用结构等等,实在是太有意思了!我必须也要让大 ...

  3. 一个故事讲明白线程的私家领地:ThreadLocal

    张大胖上午遇到了一个棘手的问题,他在一个AccountService中写了一段类似这样的代码: Context ctx = new Context(); ctx.setTrackerID(.....) ...

  4. 通过一个工具类更深入理解动态代理和Threadlocal

    动态代理和Threadlocal 一个代理类返回指定的接口,将方法调用指定的调用处理程序的代理类的实例.返回的是一个代理类,由指定的类装载器的定义和实现指定接口指定代理实例调用处理程序最近用到一个工具 ...

  5. 一个错误使用单例模式的场景及ThreadLocal简析

    近来参与一个Java的web办公系统,碰到一个bug,开始猜测是线程池管理的问题,最后发现是单例模式的问题. 即,当同时发起两个事务请求时,当一个事务完成后,另一个事务会抛出session is cl ...

  6. 正确理解ThreadLocal

    想必很多朋友对 ThreadLocal并不陌生,今天我们就来一起探讨下ThreadLocal的使用方法和实现原理.首先,本文先谈一下对ThreadLocal的理 解,然后根据ThreadLocal类的 ...

  7. 蚂蚁金服研发的金融级分布式中间件SOFA背后的故事

    导读:GIAC大会期间,蚂蚁金服杨冰,黄挺等讲师面向华南技术社区做了<数字金融时代的云原生架构转型路径>和<从传统服务化走向Service Mesh>等演讲,就此机会,高可用架 ...

  8. 【转载】 Java并发编程:深入剖析ThreadLocal

    原文链接:http://www.cnblogs.com/dolphin0520/p/3920407.html感谢作者的辛苦总结! Java并发编程:深入剖析ThreadLocal 想必很多朋友对Thr ...

  9. 蚂蚁金服SOFAMesh在多语言上的实践

    在用一项技术前,一定要知道它的优点和缺点,它的优点是否对你有足够的吸引力,它的缺点不足你是否有办法补上.黄挺在CNUTCon全球运维大会上的分享也很不错. 黄挺,蚂蚁金服高级技术专家,蚂蚁金服分布式架 ...

  10. threading模块、ThreadLocal

    一.threading模块 1.线程对象的创建 1.1 Thread类直接创建 import threading import time def countNum(n): # 定义某个线程要运行的函数 ...

随机推荐

  1. springboot线程池的使用方式2

    一.简单介绍 方式1:Executors.newCachedThreadPool线程池.Executors有7种不同的线程池. private static final ExecutorService ...

  2. Serverless 时代开启,云计算进入业务创新主战场

    作者 | 于洪涛 "我们希望让用户做得更少而收获更多,通过 Serverless 化,让企业使用云服务像用电一样简单." Serverless 化正在成为全新的软件研发范式,阿里云 ...

  3. java 将字符串变成小写 单引号内的字符串大小写不变

    public static void main(String[] args) { String str = "asdfFFFSSDAF'aaaaAAA','132213'"; Sy ...

  4. 深度学习基础课:使用Adam算法

    大家好~我开设了"深度学习基础班"的线上课程,带领同学从0开始学习全连接和卷积神经网络,进行数学推导,并且实现可以运行的Demo程序 线上课程资料: 本节课录像回放 加QQ群,获得 ...

  5. java进阶(27)--HashSet与TreeSet

    一.HashSet: 1.特点:无序不可重复,实际上为放入HashMap中的key部分. 2.举例说明:

  6. 有了Composition API后,有些场景或许你不需要pinia了

    前言 日常开发时有些业务场景功能很复杂,如果将所有代码都写在一个vue组件中,那个vue文件的代码量可能就几千行了,维护极其困难.这时我们就需要将其拆分为多个组件,拆完组件后就需要在不同组件间共享数据 ...

  7. 百度网盘(百度云)SVIP超级会员共享账号每日更新(2024.01.21)

    一.百度网盘SVIP超级会员共享账号 可能很多人不懂这个共享账号是什么意思,小编在这里给大家做一下解答. 我们多知道百度网盘很大的用处就是类似U盘,不同的人把文件上传到百度网盘,别人可以直接下载,避免 ...

  8. [转帖]SPEC CPU 2017 单线程整数性能测试与总结 (2022)

    https://zhuanlan.zhihu.com/p/574105237 x86处理器的整数性能在过去4年间取得了长足的进步 x86处理器移动端性能缩水非常严重 ARM公版的旗舰级处理器相比前代进 ...

  9. [转帖]oracle通过pid查找执行SQL

    通过TOP 命令查看PID:1560 PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 1560 oracle 20 0 38.978g 0. ...

  10. [转帖]tidb-lightning 逻辑模式导入

    https://docs.pingcap.com/zh/tidb/stable/tidb-lightning-configuration 本文档介绍如何编写逻辑导入模式的配置文件,如何进行性能调优等内 ...