并发编程之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不能是弱引用

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

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

相关文章

JavaScript(七)-高级技巧篇

文章目录 深浅拷贝浅拷贝深拷贝 异常处理thorw抛异常try/catch捕获异常debugger 处理thisthis指向改变this 性能优化防抖lodash实现防抖手写防抖函数 节流 - throttle 深浅拷贝 浅拷贝 深拷贝 深拷贝有三种方式 通过递归实现深拷贝 一定先写数组再写对象 lodash/cloneDeep …

SpringBoot+FreeMaker

目录 1.FreeMarker说明2.SpringBootFreeMarker快速搭建Pom文件application.properties文件Controller文件目录结构 3.FreeMarker数据类型3.1.布尔类型3.2.数值类型3.3.字符串类型3.4.日期类型3.5.空值类型3.6.sequence类型3.7.hash类型 4.FreeMarker指令assign自定义变量指令if…

解放双手,批量绕过403

将dirsearch扫描出来的结果复制到url.txt&#xff0c;如下所示 url.txt [21:18:16] 502 - 0B - /var/log/exception.log [21:18:21] 502 - 0B - /WEB-INF/jetty-env.xml [21:18:22] 502 - 0B - /WEB-INF/weblogic.xml [21:18:27] 502 - 0B - /wp-json/wp/v2/u…

【信号与系统 - 8】取样定理

1 定义 取样处理就是对连续信号的离散化处理 p ( t ) p(t) p(t) 是开关函数 f s ( t ) f ( t ) ⋅ p ( t ) f_s(t)f(t)\cdot p(t) fs​(t)f(t)⋅p(t) 当 p ( t ) p(t) p(t) 为周期矩形函数时 该取样为均匀抽样&#xff0c;周期为 T s T_s Ts​&#xff0c;则取样角频率为&…

一种驱动器的功能安全架构介绍

下图提供了驱动器实现安全功能的架构 具有如下特点&#xff1a; 1.通用基于总线或者非总线的架构。可以实现ethercat的FSOE&#xff0c;profinet的profisafe&#xff0c;或者伺服本体安全DIO现实安全功能。 2.基于1oo2D架构&#xff0c;安全等级可以达到sil3。 3.高可用性。单…

keil无法查看外设寄存器(生成SFR文件)

1.前言 自从更新了keil&#xff0c;用的是越来越不顺手了&#xff0c;一会是cannot evaluate&#xff0c;一会是与强制与cubemx强制联系了&#xff0c;这次也是的&#xff08;地铁&#xff0c;老人&#xff0c;手机&#xff09;折腾了一下总是搞好了&#xff08;网上的解法只能…

Google最新论文: 复杂的 Prompt 如何更好的调试?

本文介绍了Sequence Salience&#xff0c;这是一个专为调试复杂的大模型提示而设计的系统。该系统利用广泛使用的显著性方法&#xff0c;支持文本分类和单标记预测&#xff0c;并将其扩展到可处理长文本的调试系统。现有的工具往往不足以处理长文本或复杂提示的调试需求。尽管存…

InternlM2

第一次作业 基础作业 进阶作业 1. hugging face下载 2. 部署 首先&#xff0c;从github上git clone仓库 https://github.com/InternLM/InternLM-XComposer.git然后里面的指引安装环境

微信小程序开发遇到的奇奇怪怪的问题

新建项目发现顶部栏标题不生效问题 开发者工具新建项目默认开启全局Skyline渲染引擎&#xff0c;因为Skyline不支持原生导航栏&#xff0c;所以就没显示原生导航栏了。 如果想用回原生导航栏&#xff0c;可以把app.json里面的 "renderer": "skyline", 去掉…

Docker容器基本操作从入门到大牛

1. Docker安装部署 1.1 openEuler使用YUM安装 [rootnode1 ~]# yum install docker -y [rootnode1 ~]# docker version Client:Version: 18.09.0EulerVersion: 18.09.0.332API version: 1.39Go version: go1.17.3Git commit: 9942888Built…

自定义多数据源

多数据源 第一章 自定义多数据源 文章目录 多数据源前言一、先在配置文件中配置好多个数据源二、配置数据源的配置文件三、定义动态数据源配置1、自定义了Datasource&#xff0c;主要目的是为了在Spring容器中定义一个datasource的Bean&#xff0c;用于mybtais获取数据库连接使…

网络安全等级保护等级测评方案

软件全套资料包获取进主页。 ‘