全面剖析一下ThreadLocal

在这里插入图片描述
什么是ThreadLocal?
ThreadLocal英文翻译过来就是:线程本地量,它其实是一种线程的隔离机制,保障了多线程环境下对于共享变量访问的安全性。

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

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

便于理解,我们看一下下图。
在这里插入图片描述
至于上述图中提及的threadLocals(ThreadLocalMap),我们后文看源代码的时候再继续来看。大家心中暂时有个概念。

既然都是保证线程访问的安全性,那么和Synchronized区别是什么呢?
在上面聊到共享变量访问安全性的问题上,其实大家还会很容易想起另外一个关键字Synchronized。聊聊区别吧,整理了一张图,看起来可能会更加直观一些,如下。
在这里插入图片描述
通过上图,我们发现ThreadLocal其实是一种线程隔离机制。Synchronized则是一种基于Happens-Before规则里的监视器锁规则从而保证同一个时刻只有一个线程能够对共享变量进行更新。

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

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

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的方式将其优化一下。代码如下:

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呢?

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

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

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

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

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

升华篇
ThreadLocal原理
上述其实咱们聊得相对而言还是比较浅的。那么接下来,咱们丰富一下之前提到的结构图,从源代码侧深度剖一下ThreadLocal吧。
在这里插入图片描述
对应上述图中,解释一下。

图中有两个线程Thread1以及Thread2。
Thread类中有一个叫做threadLocals的成员变量,它是ThreadLocal.ThreadLocalMap类型的。
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默认值为16int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //key和16取得哈希值table[i] = new Entry(firstKey, firstValue);//创建节点,设置key-valuesize = 1;setThreshold(INITIAL_CAPACITY); //设置扩容阈值}}

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

set方法如下:

 public void set(T value) {Thread t = Thread.currentThread(); //获取当前线程tThreadLocalMap map = getMap(t);  //根据当前线程获取到ThreadLocalMapif (map != null)  //如果获取的ThreadLocalMap对象不为空map.set(this, value); //K,V设置到ThreadLocalMap中elsecreateMap(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、获取当前线程的ThreadLocalMapThreadLocalMap 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,值为nullreturn setInitialValue();}private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;}

remove方法:

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

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

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

Thread线程类有一个类型为ThreadLocal.ThreadLocalMap的变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。
ThreadLocalMap方法内部维护着Entry数组,其中key是ThreadLocal本身,而value则为其泛型值。
并发场景下,每个线程都会存储当前变量副本到自己的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也会被回收。这话是没错。但是咱们的线程是计算机珍贵资源,为了避免重复创建线程带来开销,系统中我们往往会使用线程池(线程池传送门),如果使用线程池的话,那么线程的生命周期就被拉长了,那么就可想而知了。

如何避免
解法如下:

每次使用完毕之后记得调用一下remove()方法清除数据。
ThreadLocal变量尽量定义成static final类型,避免频繁创建ThreadLocal实例。这样可以保证程序中一直存在ThreadLocal强引用,也能保证任何时候都能通过ThreadLocal的弱引用访问Entry的value值,从而进行清除。
不过话说回来,其实ThreadLocal内部也做了优化的。在set()的时候也会采样清理,扩容的时候也会检查(这里希望大家自己深入看一下源代码),在get()的时候,如果没有直接命中或者向后环形查找的时候也会进行清理。但是为了系统的稳健万无一失,所以大家尽量还是将上面的两个注意点在写代码的时候注意下。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/518021.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【趣玩一下】StreamDiffusion一秒100张!实时生成二次元老婆照!

源代码 https://github.com/cumulo-autumn/StreamDiffusion 基础原理 首先Stream Batch&#xff0c;是将原来顺序的去噪步骤改为批量化处理。允许在一个批处理中&#xff0c;每幅图像处于去噪流程的不同阶段。 如此一来&#xff0c;可以大大减少UNet推理次数&#xff0c;显著…

C# 操作LiteDB

1、很简单的东西不废话&#xff0c;直接上图上代码。 2、NuGet程序中根据自己的项目版本安装LiteDB,如下图&#xff1a; 3、程序运行加过如下图&#xff1a; 4、程序代码如下&#xff1a; using System; using System.Collections.Generic; using System.Linq; using System…

电脑e盘文件没删除但不见了怎么办?了解原因与找回方法

电脑E盘文件没删除但不见了怎么办&#xff1f;在数字化时代&#xff0c;电脑已成为我们生活和工作中不可或缺的工具。其中&#xff0c;各个硬盘分区更是承载着我们宝贵的数据。但有时&#xff0c;你可能会发现&#xff0c;存放在E盘中的某些文件明明没有删除&#xff0c;却突然…

(黑马出品_04)SpringCloud+RabbitMQ+Docker+Redis+搜索+分布式

&#xff08;黑马出品_04&#xff09;SpringCloudRabbitMQDockerRedis搜索分布式 微服务技术异步通信 今日目标1.初识MQ1.1.同步和异步通讯1.1.1.同步通讯1.1.2.异步通讯 1.2.技术对比 2.快速入门2.1.安装RabbitMQ2.1.1.单机部署(1).下载镜像方式…

java用Scanner读取文件内容

File file new File("./Day12/src/tmp.txt");Scanner scanner new Scanner(file);System.out.println(scanner.nextLine());注意文件路径是从项目根目录开始。

Tomcat介绍在IDEA中创建JavaWeb工程

文章目录 一、WEB服务器服务器概述使用Java代码手写web服务器 二、服务器软件Web服务器服务器软件的使用步骤 三、TomcatTomcat的下载Tomcat的安装与卸载Tomcat的启动与关闭常见问题 四、新建Java Web项目并将项目部署到tomcat中新建Java Web项目将项目部署到Tomcat中出现的问题…

Python 文件基础科普与文件打开技术详解【第117篇—文件打开技术】

Python 文件基础科普与文件打开技术详解 在Python编程中&#xff0c;文件操作是一项基础而重要的任务。无论是读取数据、写入文件还是进行其他文件处理操作&#xff0c;都需要对文件基础知识有一定的了解。在本文中&#xff0c;我们将首先介绍Python文件的基础概念&#xff0c…

【视频转码】基于RK3588的视频转码探索

传统的视频转码服务基本都是基于X86下CPU、GPU转码&#xff0c;对硬件性能、功耗、成本来说都比较高。从技术角度来说现有视频转码技术有&#xff1a; 视频编码转变&#xff1a; 1. H.264 > H.265 保持视频分辨率、清晰度不变情况下&#xff0c;更改视频压缩方式&#xff0…

【洛谷 P8748】[蓝桥杯 2021 省 B] 时间显示 题解(数学+模运算+输入输出)

[蓝桥杯 2021 省 B] 时间显示 题目描述 小蓝要和朋友合作开发一个时间显示的网站。在服务器上&#xff0c;朋友已经获取了当前的时间&#xff0c;用一个整数表示&#xff0c;值为从 1970 年 1 月 1 日 00:00:00 到当前时刻经过的毫秒数。 现在&#xff0c;小蓝要在客户端显示…

精品中国货出海wordpress外贸独立站建站模板

旗袍唐装wordpress外贸网站模板 旗袍、唐装、华服wordpress外贸网站模板&#xff0c;适合做衣服生意的外贸公司官网使用。 https://www.jianzhanpress.com/?p3695 劳动防护wordpress外贸独立站模板 劳动防护wordpress外贸独立站模板&#xff0c;劳动保护、劳动防护用品外贸…

Docker-自定义镜像

目录 1 前言 2 构建java应用的步骤及镜像结构图 2.1 构建步骤 2.2 镜像结构图 3 Dockerfile常用指令 4 Dockerfile的内容举例 4.1 一般形式 4.2 一般形式的优化 5 构建镜像 5.1 指令 5.2 实操 5.2.1 加载jdk镜像(基础镜像) 5.2.2 构建我们的镜像 5.2.3 使用我们的…

WPF LinearGradientBrush立体效果

WPF LinearGradientBrush立体效果 渐变方向 1. 默认是左上角到右下角 2.从左到右 <Border Height"35" Width"120"><Border.Background><LinearGradientBrush EndPoint"1,0"><GradientStop Color"Yellow"Offs…