当前位置: 首页 > 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贴图和…...

挑战杯推荐项目

“人工智能”创意赛 - 智能艺术创作助手&#xff1a;借助大模型技术&#xff0c;开发能根据用户输入的主题、风格等要求&#xff0c;生成绘画、音乐、文学作品等多种形式艺术创作灵感或初稿的应用&#xff0c;帮助艺术家和创意爱好者激发创意、提高创作效率。 ​ - 个性化梦境…...

(十)学生端搭建

本次旨在将之前的已完成的部分功能进行拼装到学生端&#xff0c;同时完善学生端的构建。本次工作主要包括&#xff1a; 1.学生端整体界面布局 2.模拟考场与部分个人画像流程的串联 3.整体学生端逻辑 一、学生端 在主界面可以选择自己的用户角色 选择学生则进入学生登录界面…...

【网络安全产品大调研系列】2. 体验漏洞扫描

前言 2023 年漏洞扫描服务市场规模预计为 3.06&#xff08;十亿美元&#xff09;。漏洞扫描服务市场行业预计将从 2024 年的 3.48&#xff08;十亿美元&#xff09;增长到 2032 年的 9.54&#xff08;十亿美元&#xff09;。预测期内漏洞扫描服务市场 CAGR&#xff08;增长率&…...

Linux相关概念和易错知识点(42)(TCP的连接管理、可靠性、面临复杂网络的处理)

目录 1.TCP的连接管理机制&#xff08;1&#xff09;三次握手①握手过程②对握手过程的理解 &#xff08;2&#xff09;四次挥手&#xff08;3&#xff09;握手和挥手的触发&#xff08;4&#xff09;状态切换①挥手过程中状态的切换②握手过程中状态的切换 2.TCP的可靠性&…...

为什么需要建设工程项目管理?工程项目管理有哪些亮点功能?

在建筑行业&#xff0c;项目管理的重要性不言而喻。随着工程规模的扩大、技术复杂度的提升&#xff0c;传统的管理模式已经难以满足现代工程的需求。过去&#xff0c;许多企业依赖手工记录、口头沟通和分散的信息管理&#xff0c;导致效率低下、成本失控、风险频发。例如&#…...

Ascend NPU上适配Step-Audio模型

1 概述 1.1 简述 Step-Audio 是业界首个集语音理解与生成控制一体化的产品级开源实时语音对话系统&#xff0c;支持多语言对话&#xff08;如 中文&#xff0c;英文&#xff0c;日语&#xff09;&#xff0c;语音情感&#xff08;如 开心&#xff0c;悲伤&#xff09;&#x…...

【HTTP三个基础问题】

面试官您好&#xff01;HTTP是超文本传输协议&#xff0c;是互联网上客户端和服务器之间传输超文本数据&#xff08;比如文字、图片、音频、视频等&#xff09;的核心协议&#xff0c;当前互联网应用最广泛的版本是HTTP1.1&#xff0c;它基于经典的C/S模型&#xff0c;也就是客…...

七、数据库的完整性

七、数据库的完整性 主要内容 7.1 数据库的完整性概述 7.2 实体完整性 7.3 参照完整性 7.4 用户定义的完整性 7.5 触发器 7.6 SQL Server中数据库完整性的实现 7.7 小结 7.1 数据库的完整性概述 数据库完整性的含义 正确性 指数据的合法性 有效性 指数据是否属于所定…...

Go 并发编程基础:通道(Channel)的使用

在 Go 中&#xff0c;Channel 是 Goroutine 之间通信的核心机制。它提供了一个线程安全的通信方式&#xff0c;用于在多个 Goroutine 之间传递数据&#xff0c;从而实现高效的并发编程。 本章将介绍 Channel 的基本概念、用法、缓冲、关闭机制以及 select 的使用。 一、Channel…...

腾讯云V3签名

想要接入腾讯云的Api&#xff0c;必然先按其文档计算出所要求的签名。 之前也调用过腾讯云的接口&#xff0c;但总是卡在签名这一步&#xff0c;最后放弃选择SDK&#xff0c;这次终于自己代码实现。 可能腾讯云翻新了接口文档&#xff0c;现在阅读起来&#xff0c;清晰了很多&…...