当前位置: 首页 > news >正文

并发编程之ThreadLocal使用及原理

ThreadLocal主要是为了解决线程安全性问题的

非线程安全举例

public class ThreadLocalDemo {// 非线程安全的private static final SimpleDateFormat sdf= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public static Date parse(String strDate) throws ParseException {return sdf.parse(strDate);}public static void main(String[] args) {ExecutorService executorService = Executors.newFixedThreadPool(10);for (int i = 0; i < 20; i++) {executorService.execute(() -> {try {System.out.println(parse("2021-05-30 20:21:20"));} catch (ParseException e) {e.printStackTrace();}});}}
}

以上代码,构造了一个SimpleDateFormat对象,然后在main线程中,开启了20个线程执行时间的格式化,其输出结构部分如下:

可以看到,程序有部分线程打印出了结果,但结果也都不一样,并且也有部分报了异常。

分析:这是由于SimpleDateFormat在这里设置的是一个全局变量,多个线程共用的时候,必然涉及到共享资源的抢占,其parse方法内部对字符串的处理的操作就是非原子性的,因此就会出现真正执行的时候,拿到的最终字符串无法确定,导致以上报错。

思考: 如何修改? 使用ThreadLocal将DateFormat设置为线程安全的,保证每个线程的操作都是原子的。

ThreadLocal应用

public class ThreadLocalDemo {private static final ThreadLocal<DateFormat> dateFormatThreadLocal = new ThreadLocal<>();public static Date parse(String strDate) throws ParseException {DateFormat dateFormat = dateFormatThreadLocal.get();if (dateFormat == null ) {dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");dateFormatThreadLocal.set(dateFormat);}return dateFormat.parse(strDate);}public static void main(String[] args) {ExecutorService executorService = Executors.newFixedThreadPool(10);for (int i = 0; i < 20; i++) {executorService.execute(() -> {try {System.out.println(parse("2024-04-12 15:12:20"));} catch (ParseException e) {e.printStackTrace();}});}}
}

结果输出

ThreadLocal常用方法

set()

在当前线程范围内,设置一个值存储在ThreadLocal中,这个值仅对当前线程可见

相当于在当前线程范围内建立了副本

get()

从当前线程范围内取出set()方法设置的值

remove()

移除当前线程范围内的值

ThreadLocal原理猜想

1. 能够实现线程的隔离,当前保存的数据,只会存储在当前的线程范围内->线程内私有的

2.有一个存储结构

3.key->保存当前线程

ThreadLocal源码分析

1.初始化ThreadLocalMap

        static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}

作为一个静态内部类,在类加载时,就会加载该线程的一个ThreadLocalMap.Entry

注意:在这里Entry采用的是一个弱引用对象,为什么要采用弱引用对象呢?这是由于ThreadMap是和线程绑在一起的,如果这个线程没有被销毁,而我们又已经不会在使用ThreadLocal引用了,那么key-value的键值对就会一直在map中存在,这对于程序来说,就出现了内存泄漏。为了避免这种情况,只要将Key设置为弱引用,那么当发生GC的时候,就会自动的把弱引用给清理掉了。

2. set主逻辑源码

   public void set(T value) {// 1. 获得当前线程tThread t = Thread.currentThread();// 2. 取当前线程的ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null) {// 2.1 如果map不为空, 则设置当前ThreadLocal变量的value值map.set(this, value);} else {// 2.2 若map为空,则创建一个ThreadLocalMapcreateMap(t, value);}}

1.2 当前线程的map不为空时,如何进行set值

    private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;int len = tab.length;// 2.1.1 根据当前key获得索引值int i = key.threadLocalHashCode & (len-1);// 2.1.2 循环entry数组for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {// 2.1.3 entry不为空 获得当前entry数组元素的keyThreadLocal<?> k = e.get();// 2.1.4 若相等,则赋值valueif (k == key) {e.value = value;return;}// 2.1.5若当前entry中为空 则进行替换空余的数组if (k == null) {replaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)// 2.1.6 这里主要是对Entry数组的一个扩容知识 rehash();}

分析:当前entry为空(key值可能被GC回收了),那么该条数据就可能为脏数据,脏Entry,只有value有值 key为null  就执行replaceStaleEntry方法(注:2.1.6在这里不再展开,主要是Entry数组的一个扩容)

     private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {Entry[] tab = table;int len = tab.length;Entry e;// a.当前key的索引int slotToExpunge = staleSlot;// b. 向前查询脏Entry  for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len))if (e.get() == null)slotToExpunge = i;// c. 向后查找可覆盖的Entryfor (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();if (k == key) {e.value = value;// d.进行交换 避免Entry中数据重复tab[i] = tab[staleSlot];tab[staleSlot] = e;if (slotToExpunge == staleSlot)slotToExpunge = i;// e.从前往后清理脏EntrycleanSomeSlots(expungeStaleEntry(slotToExpunge), len);return;}if (k == null && slotToExpunge == staleSlot)slotToExpunge = i;}// f. 没有找到可覆盖的Entry,则清理当前索引的value重新赋值tab[staleSlot].value = null;tab[staleSlot] = new Entry(key, value);// h.清理查询到的脏Entryif (slotToExpunge != staleSlot)cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);}

该段代码其实会分为4种情况

a. 向前查找有脏Entry 向后查找到可覆盖的Entry(这里是由于存储的时候会有哈希冲突,因此向后可能会有相同的key值)

b.向前有脏Entry向后未找到可覆盖的Entry(则直接在当前索引位置直接赋值新的Entry)

c.向前没有脏Entry向后找到可覆盖的Entry

d.向前没有脏Entry向后未找到可覆盖的Entry

分析:上述set值,一定程度上避免了Entry数组的内存泄漏,因为可以向前检索到脏Entry并进行清理,但是如果向前查找提前停了下来,那么前面仍还有脏Entry未扫描到,那么仍会有部分内存泄漏。真正全部清理需要每次使用后调用remove方法

1.3 当前线程的Map为空时,进行创建并set值

    void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}
       ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {// 2.2.1 初始化一个16长度大小的数组table = new Entry[INITIAL_CAPACITY];// 2.2.2 设置下标索引 采用的是线性探测法int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);// 2.2.3 将Entry设置到该数组索引处table[i] = new Entry(firstKey, firstValue);size = 1;// 2.2.4 设置阈值为10setThreshold(INITIAL_CAPACITY);}

设置索引的地方主要是采用线性探测法来解决哈希冲突,找到一篇不错的博客,感兴趣可以参考博客:ThreadLocalMap线性探测法解决hash冲突_thread t = thread.currentthread(); threadlocalmap -CSDN博客

3. get主逻辑源码

    public T get() {Thread t = Thread.currentThread();// 1.首先获得当前线程的ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null) {// 2.拿到Entry如何Entry不为空的情况下 直接返回值ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}// 3. 如果弱引用key被回收了,则会重新创建当前线程的Entry,并赋值return setInitialValue();}

4.remove主逻辑源码

     private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {if (e.get() == key) {// 遍历当前Entry数组,全部清理掉e.clear();expungeStaleEntry(i);return;}}}

ThreadLocal总结

1.ThreadLocal主要是为了线程安全,避免多线程的资源共享,线程间的资源互相隔离

2..ThreadLocal的注意点: ThreadLocal可能会造成内存泄漏,因此在每次使用完后,调用remove进行清理

3.为什么ThreadLocal的key值是弱应用,而value值是强引用? 在ThreadLocalMap初始化时已经说明了key值为什么要采用弱引用,那么value值为什么不能设置为弱引用呢。假设Entry的key所引用的ThreadLocal对象还被其他的引用对象强引用着,那么这个ThreadLocal对象就不会被GC回收,但如果value是弱引用且不被其他引用对象引用着,那么GC的时候就会被回收掉了。那线程通过ThreadLocal获取value的时候就会获得null,ThreadLocal显然就是用来关联value的,value才是我们要保存的值,如果value都没了,还用ThreadLocal干嘛。所以value不能是弱引用

相关文章:

并发编程之ThreadLocal使用及原理

ThreadLocal主要是为了解决线程安全性问题的 非线程安全举例 public class ThreadLocalDemo {// 非线程安全的private static final SimpleDateFormat sdf new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public static Date parse(String strDate) throws ParseExc…...

软件测试 测试开发丨Pytest结合数据驱动-yaml,熬夜整理蚂蚁金服软件测试高级笔试题

编程语言 languages: PHPJavaPython book: Python入门: # 书籍名称 price: 55.5 author: Lily available: True repertory: 20 date: 2018-02-17 Java入门: price: 60 author: Lily available: False repertory: Null date: 2018-05-11 yaml 文件使用 查看 yaml 文件 pych…...

软考数据库---2.SQL语言

主要记忆&#xff1a;表、索引、视图操作语句&#xff1b;数据操作&#xff1b;通配符、转义符&#xff1b;授权&#xff1b;存储过程&#xff1b;触发器 这部分等等整理一下: “”" 1、 数据定义语言。 SQL DDL提供定义关系模式和视图、 删除关系和视图、 修改关系模式的…...

基于顺序表实现通讯录

上篇我们讲了顺序表是什么&#xff0c;和如何实现顺序表。这篇文章我们将基于顺序表来实现通讯录。 文章目录 前言一、基于顺序表是如何实现的二、通讯录的头文件和实现文件三、通讯录的实现3.1 定义通讯录结构3.2 初始化通讯录3.3 销毁通讯录3.4 通讯录添加数据3.5 查找联系人…...

咸鱼之王_手游_开服搭建架设_内购修复无bug运营版

视频演示 咸鱼之王_手游_开服 游戏管理后台界面 源码获取在文章末尾 源码获取在文章末尾 源码获取在文章末尾 或者直接下面 https://githubs.xyz/y28.html 1.安装宝塔 yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh &…...

【JSON2WEB】14 基于Amis的CRUD开发30分钟速成

【JSON2WEB】系列目录 【JSON2WEB】01 WEB管理信息系统架构设计 【JSON2WEB】02 JSON2WEB初步UI设计 【JSON2WEB】03 go的模板包html/template的使用 【JSON2WEB】04 amis低代码前端框架介绍 【JSON2WEB】05 前端开发三件套 HTML CSS JavaScript 速成 【JSON2WEB】06 JSO…...

Java入门教程||Java 变量

Java 变量 Java教程 - Java变量 变量由标识符&#xff0c;类型和可选的初始化程序定义。变量还具有范围&#xff08;可见性/生存期&#xff09;。 Java变量类型 在Java中&#xff0c;必须先声明所有变量&#xff0c;然后才能使用它们。变量声明的基本形式如下所示&#xff1…...

基于Java的校园快递一站式服务系统 (源码+文档+包运行)

一.系统概述 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本校园快递一站式服务系统就是在这样的大环境下诞生&#xff0c;其可以帮助管理者在短时间内处理完毕庞…...

通讯录的实现(顺序表版本)

我们知道通讯录是基于顺序表的前提下&#xff0c;要写好通讯录我们就要深入了解好顺序表。我们先来看看什么是顺序表。&#xff08;注意今天代码量有点多&#xff0c;坚持一下&#xff09;。冲啊&#xff01;兄弟们&#xff01; 顺序表的简单理解 对于顺序表&#xff0c;我们首…...

利用Sentinel解决雪崩问题(一)流量控制

1、解决雪崩问题的常见方式有四种: 超时处理:设定超时时间&#xff0c;请求超过一定时间没有响应就返回错误信息&#xff0c;不会无休止等待;舱壁模式:限定每个业务能使用的线程数&#xff0c;避免耗尽整个tomcat的资源&#xff0c;因此也叫线程隔离;熔断降级:由断路器统计业务…...

二叉树总结

递归返回值 1、如果需要搜索整棵二叉树且不用处理递归返回值&#xff0c;递归函数就不要返回值。 2、如果需要搜索整棵二叉树且需要处理递归返回值&#xff0c;递归函数就需要返回值。 3、如果要搜索其中一条符合条件的路径&#xff0c;那么递归一定需要返回值&#xff0c;…...

接口优化技巧

一、背景 针对老项目&#xff0c;去年做了许多降本增效的事情&#xff0c;其中发现最多的就是接口耗时过长的问题&#xff0c;就集中搞了一次接口性能优化。本文将给小伙伴们分享一下接口优化的通用方案 二、接口优化方案总结 1.批处理 批量思想&#xff1a;批量操作数据库&a…...

【工具】NPS 内网穿透搭建

背景 在日常开发中经常会涉及到使用公网某个端口进行开发调试的情况&#xff0c;但我们日常开发的机器IP是非公网IP&#xff0c;所以需要使用内网穿透的手段&#xff0c;使我们的服务在公网上能被访问到。 常用的内网穿透工具分两大类&#xff0c;一类是付费/免费服务&#xf…...

【数学】主成分分析(PCA)的详细深度推导过程

本文基于Deep Learning (2017, MIT)&#xff0c;推导过程补全了所涉及的知识及书中推导过程中跳跃和省略的部分。 blog 1 概述 现代数据集&#xff0c;如网络索引、高分辨率图像、气象学、实验测量等&#xff0c;通常包含高维特征&#xff0c;高纬度的数据可能不清晰、冗余&am…...

微信跳转页面时发生报错

报错如下图所示&#xff1a; 解决方法&#xff1a;&#xff08;从下面四种跳转方式中任选一种&#xff0c;哪种能实现效果就用哪个&#xff09; 带历史回退 wx.navigateTo() //不能跳转到tabbar页面 不带历史回退 wx.redirectTo() //跳转到另一个页面wx.switchTab() //只能…...

8:系统开发基础--8.1:软件工程概述、8.2:软件开发方法 、8.3:软件开发模型、8.4:系统分析

转上一节&#xff1a; http://t.csdnimg.cn/G7lfmhttp://t.csdnimg.cn/G7lfm 课程内容提要&#xff1a; 8&#xff1a;知识点考点详解 8.1&#xff1a;软件工程概述 1.软件的生存周期 2.软件过程改进—CMM Capability Maturity Model能力成熟度模型 3.软件过程改进—CMMI—…...

【简单讲解下Symfony框架】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…...

[Linux基础]ln硬链接和ln -s软链接的方法参数及区别

区别&#xff1a; 1、ln创建硬链接&#xff1b;ln -s 创建软链接 2、硬链接的两个文件指向同一个inode&#xff08;inode&#xff1a;存放着文件的目录、权限、block块编号等信息&#xff09;&#xff1b;软链接的目标文件指向源文件&#xff0c;目标文件内存储的是源文件的目…...

开源博客项目Blog .NET Core源码学习(15:App.Hosting项目结构分析-3)

本文学习并分析App.Hosting项目中前台页面的关于本站页面和点点滴滴页面。 关于本站页面 关于本站页面相对而言布局简单&#xff0c;与后台控制器类的交互也不算复杂。整个页面主要使用了layui中的面包屑导航、选项卡、模版、流加载等样式或模块。   面包屑导航。使用layui…...

【muzzik 分享】3D模型平面切割

# 前言 一年一度的征稿到了&#xff0c;倒腾点存货&#xff0c;3D平面切割通常用于一些解压游戏里&#xff0c;例如水果忍者&#xff0c;切菜这些&#xff0c;今天我就给大家讲讲怎么实现3D切割以及其原理&#xff0c;帮助大家更理解3D中的 Mesh(网格)&#xff0c;以及UV贴图和…...

JavaSec-RCE

简介 RCE(Remote Code Execution)&#xff0c;可以分为:命令注入(Command Injection)、代码注入(Code Injection) 代码注入 1.漏洞场景&#xff1a;Groovy代码注入 Groovy是一种基于JVM的动态语言&#xff0c;语法简洁&#xff0c;支持闭包、动态类型和Java互操作性&#xff0c…...

synchronized 学习

学习源&#xff1a; https://www.bilibili.com/video/BV1aJ411V763?spm_id_from333.788.videopod.episodes&vd_source32e1c41a9370911ab06d12fbc36c4ebc 1.应用场景 不超卖&#xff0c;也要考虑性能问题&#xff08;场景&#xff09; 2.常见面试问题&#xff1a; sync出…...

使用VSCode开发Django指南

使用VSCode开发Django指南 一、概述 Django 是一个高级 Python 框架&#xff0c;专为快速、安全和可扩展的 Web 开发而设计。Django 包含对 URL 路由、页面模板和数据处理的丰富支持。 本文将创建一个简单的 Django 应用&#xff0c;其中包含三个使用通用基本模板的页面。在此…...

SkyWalking 10.2.0 SWCK 配置过程

SkyWalking 10.2.0 & SWCK 配置过程 skywalking oap-server & ui 使用Docker安装在K8S集群以外&#xff0c;K8S集群中的微服务使用initContainer按命名空间将skywalking-java-agent注入到业务容器中。 SWCK有整套的解决方案&#xff0c;全安装在K8S群集中。 具体可参…...

应用升级/灾备测试时使用guarantee 闪回点迅速回退

1.场景 应用要升级,当升级失败时,数据库回退到升级前. 要测试系统,测试完成后,数据库要回退到测试前。 相对于RMAN恢复需要很长时间&#xff0c; 数据库闪回只需要几分钟。 2.技术实现 数据库设置 2个db_recovery参数 创建guarantee闪回点&#xff0c;不需要开启数据库闪回。…...

Spark 之 入门讲解详细版(1)

1、简介 1.1 Spark简介 Spark是加州大学伯克利分校AMP实验室&#xff08;Algorithms, Machines, and People Lab&#xff09;开发通用内存并行计算框架。Spark在2013年6月进入Apache成为孵化项目&#xff0c;8个月后成为Apache顶级项目&#xff0c;速度之快足见过人之处&…...

Day131 | 灵神 | 回溯算法 | 子集型 子集

Day131 | 灵神 | 回溯算法 | 子集型 子集 78.子集 78. 子集 - 力扣&#xff08;LeetCode&#xff09; 思路&#xff1a; 笔者写过很多次这道题了&#xff0c;不想写题解了&#xff0c;大家看灵神讲解吧 回溯算法套路①子集型回溯【基础算法精讲 14】_哔哩哔哩_bilibili 完…...

汽车生产虚拟实训中的技能提升与生产优化​

在制造业蓬勃发展的大背景下&#xff0c;虚拟教学实训宛如一颗璀璨的新星&#xff0c;正发挥着不可或缺且日益凸显的关键作用&#xff0c;源源不断地为企业的稳健前行与创新发展注入磅礴强大的动力。就以汽车制造企业这一极具代表性的行业主体为例&#xff0c;汽车生产线上各类…...

【C语言练习】080. 使用C语言实现简单的数据库操作

080. 使用C语言实现简单的数据库操作 080. 使用C语言实现简单的数据库操作使用原生APIODBC接口第三方库ORM框架文件模拟1. 安装SQLite2. 示例代码:使用SQLite创建数据库、表和插入数据3. 编译和运行4. 示例运行输出:5. 注意事项6. 总结080. 使用C语言实现简单的数据库操作 在…...

JVM暂停(Stop-The-World,STW)的原因分类及对应排查方案

JVM暂停(Stop-The-World,STW)的完整原因分类及对应排查方案,结合JVM运行机制和常见故障场景整理而成: 一、GC相关暂停​​ 1. ​​安全点(Safepoint)阻塞​​ ​​现象​​:JVM暂停但无GC日志,日志显示No GCs detected。​​原因​​:JVM等待所有线程进入安全点(如…...