并发编程之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. 获得当前线程t
        Thread t = Thread.currentThread();
        // 2. 取当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 2.1 如果map不为空, 则设置当前ThreadLocal变量的value值
            map.set(this, value);
        } else {
            // 2.2 若map为空,则创建一个ThreadLocalMap
            createMap(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数组元素的key
                ThreadLocal<?> k = e.get();
                // 2.1.4 若相等,则赋值value
                if (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. 向后查找可覆盖的Entry
            for (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.从前往后清理脏Entry
                    cleanSomeSlots(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.清理查询到的脏Entry
            if (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 设置阈值为10
            setThreshold(INITIAL_CAPACITY);
        }

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

3. get主逻辑源码

    public T get() {
        Thread t = Thread.currentThread();
       // 1.首先获得当前线程的ThreadLocalMap
        ThreadLocalMap 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不能是弱引用

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Spring源码刨析之配置文件的解析和bean的创建以及生命周期

public void test1(){XmlBeanFactory xmlBeanFactory new XmlBeanFactory(new ClassPathResource("applicationContext.xml"));user u xmlBeanFactory.getBean("user",org.xhpcd.user.class);// System.out.println(u.getStu());}先介绍一个类XmlBeanFac…

服务器主机关机重启告警

提取时间段内系统操作命名&#xff0c;出现系统重启命令&#xff0c;若要出现及时联系确认 重启命令&#xff1a; reboot / init 6 / shutdown -r now&#xff08;现在重启命令&#xff09; 关机命令&#xff1a; init 0 / shutdown -h now&#xff08;关机&#…

防汛物资仓库管理系统|实现应急物资仓库三维可视化

系统概述 智慧应急物资仓库可视化系统&#xff08;智物资DW-S300&#xff09;采用了 B/S 架构的设计&#xff0c;通过浏览器即可快速登录操作。实现对库房内的应急物资从申购入库、出库、调拨、库内环境监测、维修保养、检测试验、处置报废等全周期、科学、规范的管理。系统以…

恢复MySQL!是我的条件反射,PXB开源的力量...

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是【IT邦德】&#xff0c;江湖人称jeames007&#xff0c;10余年DBA及大数据工作经验 一位上进心十足的【大数据领域博主】&#xff01;&#x1f61c;&am…

如何进行计量经济分析

计量经济分析是定量分析的常用方法&#xff0c;在经济分析领域有着广泛且重要的应用。计量经济分析以一定的经济理论和统计数据为基础&#xff0c;运用数学、统计学相关方法&#xff0c;通过建立计量模型&#xff0c;并运用软件进行操作&#xff0c;从而实现对经济问题的定量分…

时间序列模型:lag-Llama

项目地址&#xff1a;GitHub - time-series-foundation-models/lag-llama: Lag-Llama: Towards Foundation Models for Probabilistic Time Series Forecasting 论文地址&#xff1a;https://arxiv.org/pdf/2310.08278.pdf hugging-face镜像&#xff1a;https://hf-mirror.c…

QQ农场-phpYeFarm添加数据教程

前置知识 plugin\qqfarm\core\data D:\study-project\testweb\upload\source\plugin\qqfarm\core\data 也就是plugin\qqfarm\core\data是一个缓存文件,如果更新农场数据后,必须要删除才可以 解决种子限制(必须要做才可以添加成功) 你不更改加入了id大于2000直接删除种子 D…

Unity类银河恶魔城学习记录12-14 p136 Merge Skill Tree with Sword skill源代码

Alex教程每一P的教程原代码加上我自己的理解初步理解写的注释&#xff0c;可供学习Alex教程的人参考 此代码仅为较上一P有所改变的代码 【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili CharacterStats.cs using System.Collections; using System.Collections.…

如何搭建SearXNG搜索引擎

小白如何搭建SearXNG搜索引擎 前言 国内用户在使用百度、360、搜狗等主流搜索引擎时&#xff0c;面临搜索结果精确度不高、广告泛滥及隐私顾虑等问题。虽然Google以其出色性能备受推崇&#xff0c;但由于无法在国内访问&#xff0c;部分用户转而选择Bing作为折衷方案&#xff…

LeetCode617:合并二叉树

题目描述 给你两棵二叉树&#xff1a; root1 和 root2 。 想象一下&#xff0c;当你将其中一棵覆盖到另一棵之上时&#xff0c;两棵树上的一些节点将会重叠&#xff08;而另一些不会&#xff09;。你需要将这两棵树合并成一棵新二叉树。合并的规则是&#xff1a;如果两个节点重…

OSCP靶场--PayDay

OSCP靶场–PayDay 考点(公共exp文件上传密码复用sudo -l all提权) 1.nmap扫描 ## ┌──(root㉿kali)-[~/Desktop] └─# nmap -sV -sC 192.168.153.39 -p- -Pn --min-rate 2500 Starting Nmap 7.92 ( https://nmap.org ) at 2024-04-13 04:52 EDT Nmap scan report for 192…

计算机网络——ARP协议

前言 本博客是博主用于复习计算机网络的博客&#xff0c;如果疏忽出现错误&#xff0c;还望各位指正。 这篇博客是在B站掌芝士zzs这个UP主的视频的总结&#xff0c;讲的非常好。 可以先去看一篇视频&#xff0c;再来参考这篇笔记&#xff08;或者说直接偷走&#xff09;。 …

Spark-机器学习(1)什么是机器学习与MLlib算法库的认识

从这一系列开始&#xff0c;我会带着大家一起了解我们的机器学习&#xff0c;了解我们spark机器学习中的MLIib算法库&#xff0c;知道它大概的模型&#xff0c;熟悉并认识它。同时&#xff0c;本篇文章为个人spark免费专栏的系列文章&#xff0c;有兴趣的可以收藏关注一下&…

双子座 Gemini1.5和谷歌的本质

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

流媒体的安全谁来保障

流媒体的安全谁来保障 说起媒体&#xff0c;我们马上就会想到报纸新闻、广播、电视。 其实所谓的流媒体同我们通常所指的媒体是不一样的&#xff0c; 它只是一个技术名词。流媒体到底是什么&#xff1f;能给我们的生活带来什么&#xff1f;跟小德一起来看看。 流媒体是什么&a…

缓存与数据库的数据一致性解决方案分析

在现代应用中&#xff0c;缓存技术的使用广泛且至关重要&#xff0c;主要是为了提高数据访问速度和优化系统整体性能。缓存通过在内存或更快速的存储系统中存储经常访问的数据副本&#xff0c;使得数据检索变得迅速&#xff0c;从而避免了每次请求都需要从较慢的主存储&#xf…

LeetCode 0705.设计哈希集合:很多人都是这样做的吧【逃】

【LetMeFly】705.设计哈希集合&#xff1a;很多人都是这样做的吧【逃】 力扣题目链接&#xff1a;https://leetcode.cn/problems/design-hashset/ 不使用任何内建的哈希表库设计一个哈希集合&#xff08;HashSet&#xff09;。 实现 MyHashSet 类&#xff1a; void add(key…

04-03 周三 使用印象笔记API批量更新笔记标题

04-03 周三 使用印象笔记API批量更新笔记标题 时间版本修改人描述2024年4月3日11:13:50V0.1宋全恒新建文档 简介 安利印象笔记 在阅读这篇博客之前&#xff0c;首先给大家案例一下印象笔记这个应用&#xff0c;楼主之前使用onenote来记录自己的生活的&#xff0c;也记录了许多…

UI设计规范

一套商城系统的诞生&#xff0c;除了代码的编写&#xff0c;UI设计也至关重要。UI设计关系到商城系统的最终呈现效果&#xff0c;关乎整体商城的风格展现&#xff0c;如果UI设计做不好&#xff0c;带来的负面影响也是不容小觑的。 1、在很多商城系统开发中&#xff0c;有时会有…

基于Java+Vue的校园代购服务管理系统(源码+文档+包运行)

一.系统概述 在新发展的时代&#xff0c;众多的软件被开发出来&#xff0c;给用户带来了很大的选择余地&#xff0c;而且学生越来越追求更个性的需求。在这种时代背景下&#xff0c;学生对校园代购服务订单管理越来越重视&#xff0c;更好的实现校园代购服务的有效发挥&#xf…