服了,一个ThreadLocal被问出了花

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

博客: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默认值为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原理的时候,相信大家应该可以说出个所以然来。

  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()的时候,如果没有直接命中或者向后环形查找的时候也会进行清理。但是为了系统的稳健万无一失,所以大家尽量还是将上面的两个注意点在写代码的时候注意下。

总结

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

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

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

相关文章

手机gif动图怎么操作?这个方法分分钟解决

手机上怎么制作gif动画&#xff1f;Gif动图是当下非常流行的一种表达方式&#xff0c;通过简单的画面循环就能够传达您的想法。但是&#xff0c;想要在手机上制作gif动画的时候应该怎么办呢&#xff1f;通过会用GIF动图在线编辑&#xff08;https://www.gif.cn/&#xff09;工具…

【循环结构·js】

变量命名原则 变量名由字母、下划线、$ 或数字组成&#xff0c;并且必须由字母、下划线、$ 开头。 变量名不能命名为系统关键字和保留字。 JS代码在sourse里面调试 document.write(str); /*在页面上输出变量 str 的值*/数据类型的分类 为什么要标识数据类型&#xff1a; 不…

Kubernetes Pod使用

Pod是Kubernetes中可以创建、调度和部署的最小&#xff0c;也是最简单的单元。Pod是基于Kubernetes部署和运维应用的基础。本文重点介绍下Pod各字段的含义及Pod的使用&#xff0c;关于Pod更多的知识细节可以参考Kubernetes Pod详解一文。 本文参考的主要内容来源于Kubernetes 1…

设计模式——2_1 命令(Command)

文章目录 定义图纸一个例子&#xff1a;空调和他的遥控器只有控制面板的空调遥控器可以撤销的操作 碎碎念命令和Runnable命令和事务 定义 把请求封装成一个对象&#xff0c;从而使你可以用不同的请求对客户进行参数化&#xff0c;对请求排队或记录请求日志&#xff0c;以及支持…

Linux文件结构

所有Linux系统的文件结构都是一样的&#xff0c;区别于win系统的c盘&#xff0c;d盘&#xff0c;他只有一个根目录“/”&#xff0c;下面的文件夹结构基本是一样的&#xff0c;如下&#xff0c;文件夹有“箭头”代表的是软链接&#xff0c;即该文件夹的位置不在此&#xff0c;右…

小米平板6获取root权限教程

1. 绑定账号 1> 打开"设置-我的设备-全部参数-连续点击MIUI版本按钮"&#xff0c;直到提示已打开开发者模式( p s : 这里需要重点关注红框平板型号和 M I U I 版本&#xff0c;例如我这里平板型号是 X i a o m i P a d 6 &#xff0c; M I U I 版本是 14.0.10 &am…

【开源】JAVA+Vue+SpringBoot实现就医保险管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 科室档案模块2.2 医生档案模块2.3 预约挂号模块2.4 我的挂号模块 三、系统展示四、核心代码4.1 用户查询全部医生4.2 新增医生4.3 查询科室4.4 新增号源4.5 预约号源 五、免责说明 一、摘要 1.1 项目介绍 基于JAVAVue…

酷开系统 | 酷开科技智慧AI带你领略神奇的世界

在这个科技日新月异的时代&#xff0c;AI已成为我们生活中不可或缺的一部分。它不仅改变了我们的生活方式&#xff0c;更让我们对未来充满期待。说起酷开系统中智慧AI的强大&#xff0c;着实让人叹为观止。无论是语音识别、数据整理还是语言处理&#xff0c;智慧AI都在不断地突…

MySQL数据库①_MySQL入门(概念+使用)

目录 1. 数据库的概念 1.1 数据库的存储介质 1.2 主流数据库 2. MySQL的基本使用 2.1 链接数据库 2.2 服务器管理 2.3 数据库&#xff0c;服务器和表关系 2.4 简单MySQL语句 3. MySQL架构 4. SQL分类 5. 存储引擎 本篇完。 1. 数据库的概念 数据库是按照数据结构来…

测试大佬是怎么看待测试用例设计的

前言 最近干的最多的事情就是设计测试用例、评审测试用例了&#xff0c;于是我不禁又想到了一个经典的问题&#xff1a;如何设计出优秀的测试用例&#xff1f; 可能有些童鞋看到这个问题会有些不以为然&#xff0c;这有什么好想的&#xff1f;干个测试谁还不会设计测试用例&…

C++(10)——类与对象(最终篇)

目录 static成员 概念 特性 友元 友元函数 友元类 内部类 匿名对象 经过这么多天的分享&#xff0c;C的类与对象终于要结束了。结束也意味着C快要入门了。 static成员 概念 声明为static的类成员称为类的静态成员&#xff0c;用static修饰的成员变量&#xff0c;称之…

基于YOLOv8算法的照片角度分类项目实践

目录 一、任务概述二、YOLOv8算法简介2.1 算法改进2.2 算法特点2.3 网络结构2.4 性能比较 三、工程实践3.1 安装算法框架库ultralytics3.2 库存照片预处理3.2.1 提取所有图片3.2.2 去除冗余的相同照片3.2.3 去除无车辆照片3.2.4 随机提取指定数量的图片 3.3 照片朝向分类3.3.1 …

STM32--USART串口(2)串口外设

一、USART简介 可配置数据位&#xff1a;不需要校验就是8位&#xff0c;需要校验就选9位&#xff1b; 停止位&#xff1a;决定了帧的间隔; STM32F103C8T6USART&#xff1a;USART1挂载在APB2总线上&#xff0c;USART2和USART3挂载在APB1总线上&#xff1b; 二、USART框图 TXE…

excel给数据库初始化/旧数据处理(自动sql拼装)

思路&#xff1a; 首先导出数据到excel编写单条数据操作的sql利用excel CONCATENATE 函数自动生成&#xff0c;每一行数据的操作sql 小技巧:对于需要套娃的字段值&#xff0c;可以加一个临时列同样使用CONCATENATE函数进行sql拼装 案例&#xff1a; 1.临时列:CONCATENATE(C2, …

分库分表 21 条法则,hold 住!

大家好&#xff5e;今天给大家分享分库分表的 21 条法则 我们结合具体业务场景&#xff0c;以t_order表为例进行架构优化。由于数据量已经达到亿级别&#xff0c;查询性能严重下降&#xff0c;因此我们采用了分库分表技术来处理这个问题。具体而言&#xff0c;我们将原本的单库…

【Python小游戏】五子棋小游戏(完整代码)

文章目录 写在前面Tkinter简介五子棋小游戏游戏介绍程序设计运行结果注意事项写在后面写在前面 本期内容:基于tkinter开发一个五子棋小游戏 实验环境 python3.11及以上pycharmtkinterTkinter简介 Tkinter是Python中最常用的图形用户界面(GUI)库之一,用于创建窗口、对话框…

如何搭建私有云盘SeaFile并实现远程访问本地文件资料

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法 ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” #mermaid-svg-hsDnDEybLME85dTx {font-family:"trebuchet ms",verdana,arial,sans-serif;font-siz…

Web项目利用OSS进行图像存储服务

一、OSS介绍 在Web项目中&#xff0c;一些常见的功能&#xff0c;比如展示图片&#xff0c;修改头像等&#xff0c;都需要进行图片的上传操作&#xff0c;但是如果是存储在Web服务器中&#xff0c;在读取图片的时候会占用比较多的资源&#xff0c;影响服务器的性能。 常…

【数据结构】双向带头循环链表实现及总结

简单不先于复杂&#xff0c;而是在复杂之后。 文章目录 1. 双向带头循环链表的实现2. 顺序表和链表的区别 1. 双向带头循环链表的实现 List.h #pragma once #include <stdio.h> #include <assert.h> #include <stdlib.h> #include <stdbool.h>typede…

fastDFS客户端实现文件上传

一、准备工作 请确保fastDFS的tracker服务和storage服务都是处于启动状态&#xff0c;防火墙是关闭的&#xff1b; 二、具体步骤 1、pom.xml 2、让当前的微服务成为fdfs的客户端 package com.qf.config;import com.github.tobato.fastdfs.FdfsClientConfig; import org.sprin…