玩转ThreadLocal

news2024/9/25 3:24:04

前言

ThreadLocal想必都不陌生,当多线程访问同一个共享变量时,就容易出现并发问题,为了保证线程安全,我们需要对共享变量进行同步加锁,但这又带来了性能消耗以及使用者的负担,那么有没有可能当我们创建一个共享变量时,每个线程对其访问的时候访问的都是自己线程的变量呢?没错那就是ThreadLocal。

ThreadLocal使用

举个简单例子:
比如实现一些数据运算的操作,过程中可能需要借助一个临时表去处理数据,临时表有一列存的每一次的执行ID,执行完成根据此次的执行ID进行删除临时表数据。可以使用一个ThreadLocal来存储当前线程的执行ID。

public class DataSyncServiceImpl {

    private final Logger logger = LoggerFactory.getLogger(DataSyncServiceImpl.class);

    private static final ThreadLocal<String> execLocalId = ThreadLocal.withInitial(()->new String());

    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * 借助临时表进行数据运算操作
     * 临时表字段(id,execution_id)
     */
    public void calculateData(String key){
        try {
            execLocalId.set(UUID.randomUUID().toString());
            calculate();
            check();
            System.out.println("同步数据...");
        }finally {
            destory();
        }

    }

    private void calculate(){
        try {
            System.out.println("数据运算");
            String execId = execLocalId.get();
            //...
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            logger.error("执行异常!",e);
        }
    }

    private void check(){
        try {
            System.out.println("数据运算");
            String execId = execLocalId.get();
            //...
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            logger.error("执行异常!",e);
        }
    }

    private void destory(){
        //根据execution_id删除临时表数据
        StringBuffer sql = new StringBuffer();
        sql.append("delete from temp_table where execution_id = ?");
        jdbcTemplate.update(sql.toString(),execLocalId.get());
        execLocalId.remove();
    }
}

这样的话保证了每一个请求线程都有自己的执行ID,清除数据时互不影响。

ThreadLocal实现原理

进入Thread类,可以看到这样两个变量,threadLocals和inheritableThreadLocals,他们都是ThreadLocalMap类型,而ThreadLocalMap是一个类似Map的结构。默认情况下两个变量都为null,当前线程调用set或者get时才会创建。也就是说ThreadLocal变量其实是存在调用线程的内存空间中。每个Thread线程都保存了一个共享变量的副本。

1、threadLocals:当前线程的ThreadLocal变量
2、inheritableThreadLocals:解决子线程不能访问父线程中的ThreadLocal变量

ThreadLocalMap

ThreadLocalMap是一个key为ThreadLocal本身,值为存入的value,对于不同的线程,每次获取副本时,别的线程不能获取到当前线程的副本值,形成了隔离。

Thread和ThreadLocal的关系

在这里插入图片描述

Set方法源码分析

    * 设置当前线程对应的ThreadLocal的值
    * @param value 将要保存在当前线程对应的ThreadLocal的值
    */
   public void set(T value) {
       // 获取当前线程对象
       Thread t = Thread.currentThread();
       // 获取此线程对象中维护的ThreadLocalMap对象
       ThreadLocalMap map = getMap(t);
       // 判断map是否存在
       if (map != null)
           // 存在则调用map.set设置此实体entry,this这里指调用此方法的ThreadLocal对象
           map.set(this, value);
       else
           // 1)当前线程Thread 不存在ThreadLocalMap对象
           // 2)则调用createMap进行ThreadLocalMap对象的初始化
           // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
           createMap(t, value);
   }

/**
    * 获取当前线程Thread对应维护的ThreadLocalMap 
    * 
    * @param  t the current thread 当前线程
    * @return the map 对应维护的ThreadLocalMap 
    */
   ThreadLocalMap getMap(Thread t) {
       return t.threadLocals;
   }
   
   /**
    *创建当前线程Thread对应维护的ThreadLocalMap 
    * @param t 当前线程
    * @param firstValue 存放到map中第一个entry的值
    */
   void createMap(Thread t, T firstValue) {
       //这里的this是调用此方法的threadLocal
       t.threadLocals = new ThreadLocalMap(this, firstValue);
   }

执行步骤:

获取当前线程,根据当前线程获取到ThreadlocalMap,即threadLocals;
如果获取到的Map不为空,则设置value,key为调用此方法的ThreadLocal引用;
如果Map为空,则先调用createMap创建,再设置value。

Get方法源码分析

     * 返回当前线程中保存ThreadLocal的值
     * 如果当前线程没有此ThreadLocal变量,
     * 则它会通过调用{@link #initialValue} 方法进行初始化值
     * @return 返回当前线程对应此ThreadLocal的值
     */
    public T get() {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null) {
            // 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 对e进行判空 
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 获取存储实体 e 对应的 value值,即为我们想要的当前线程对应此ThreadLocal的值
                T result = (T)e.value;
                return result;
            }
        }
        /*
        	初始化 : 有两种情况有执行当前代码
        	第一种情况: map不存在,表示此线程没有维护的ThreadLocalMap对象
        	第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry
         */
        return setInitialValue();
    }

    /**
     * 初始化
     * @return the initial value 初始化后的值
     */
    private T setInitialValue() {
        // 调用initialValue获取初始化的值
        // 此方法可以被子类重写, 如果不重写默认返回null
        T value = initialValue();
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 判断map是否存在
        if (map != null)
            // 存在则调用map.set设置此实体entry
            map.set(this, value);
        else
            // 1)当前线程Thread 不存在ThreadLocalMap对象
            // 2)则调用createMap进行ThreadLocalMap对象的初始化
            // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
            createMap(t, value);
        // 返回设置的值value
        return value;
    }

执行步骤:

1、获取当前线程,获取此线程对象中维护的ThreadLocalMap对象;
2、如果Map不为空,则通过当前调用的ThreadLocal对象获取Entry;
3、判断Entry不为空,则直接返回value;
4、Map或Entry为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为Key和Value创建一个新的Map。

内存泄漏

在这里插入图片描述

从ThreadLocal整体设计上我们可以看到,key持有ThreadLocal的弱引用,GC的时候会被回收,即Entry的key为null。但是当我们没有手动删除这个Entry或者线程一直运行的前提下,存在有强引用链 threadRef->currentThread->threadLocalMap->entry -> value ,value不会被回收,导致内存泄漏。

出现内存泄漏的情况:

1、没有手动删除对应的Entry节点信息,value一直存在。
2、ThreadLocal 对象使用完后,对应线程仍然在运行。

避免内存泄露:

1、使用完ThreadLocal,调用其remove方法删除对应的Entry。
2、对于第二种情况,因为使用了弱引用,当ThreadLocal 使用完后,key的引用就会为null,而在调用ThreadLocal 中的get()/set()方法时,当判断key为null时会将value置为null,这就就会在jvm下次GC时将对应的Entry对象回收,从而避免内存泄漏问题的出现。
在这里插入图片描述

总结

本文主要讲解了ThreadLocal的作用及基本用法,以及ThreadLocal的实现原理和基础方法,注意事项。最后,用ThreadLocal一定要记得用完remove!

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

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

相关文章

只知道删除单张表的数据?不知道删除多张表的数据?

一些废话 可能在某某一天&#xff0c;你在表删除表数据的时候&#xff0c;不想一张表一张表的去删除&#xff0c;想把两个表的数据同时删除&#xff1b;然后你就会去搜索&#xff0c;然后你就很有很有很有很有可能会看到 me 的这篇优质&#xff08;呸&#xff01;&#xff01;…

HOT100--(5)最长回文子串

点击查看题目详情 中心扩散法 思路&#xff1a; 遍历字符串&#xff0c;以每个字符为中心点向两边扩散&#xff0c;如果遇到不一样的就跳出循环。以此类推&#xff0c;最后截取最大回文串返回。 细节 字符个数不一定都是奇数。当个数是偶数的是时候&#xff0c;我们可以“忽…

学习红客技术必备,手把手教你成为“安防第一人”

互联网时代已悄悄来临&#xff0c;作为新时代的人们&#xff0c;我们日常生活、工作、学习方面都需要借助互联网来完成&#xff0c;这样&#xff0c;又产生一种新的问题&#xff0c;那就是网络安全的问题&#xff0c;有时我们拼命加班好不容易完成的东西&#xff0c;在一夜之间…

优化Linux系统性能的必杀技:调整进程优先级!学会使用nice命令,让你的系统飞一般的顺畅!

文章目录前言一. nice命令介绍1.1 nice的介绍1.2 cpu资源的调度1.3 nice是控制内核吗&#xff1f;二. 语法格式及常用选项三. 参考案例3.1 将ls命令的优先级调整为最高3.2 将 find 命令的优先级调整为最低3.3 如何查看nice值四. nice和renice的区别总结前言 大家好&#xff0c…

Zookeeper的安装

目录 Zookeeper的安装 1、环境准备 2、上传 3、解压文件到opt/zookeeper目下 4、安装完后进入zookeeper&#xff0c;找到conf目录 5、复制zoo_sample.cfg 6、编辑zoo.cfg 7、复制一份会话&#xff0c;进入zookeeper安装目录&#xff0c;创建一个文件夹zkdata&#xff0…

Linux·DMA 与零拷贝技术

DMA 与零拷贝技术注意事项&#xff1a;除了 Direct I/O&#xff0c;与磁盘相关的文件读写操作都有使用到 page cache 技术。1. 数据的四次拷贝与四次上下文切换很多应用程序在面临客户端请求时&#xff0c;可以等价为进行如下的系统调用&#xff1a;File.read(file, buf, len);…

NOC2021年测试卷3

1. 角色初始位置坐标是(0,0),执行下面程序后,角色会出现在什么位置上?( ) A. x坐标为10,y坐标为50B. x坐标为40,y坐标为50C. x坐标为50,y坐标为40D. x坐标为30,y坐标为502. 执行下面程序后,按一次→键,兔子会?() A. 向右移动10步B. 向左移动10步C. 向上移动…

入职外包三个月,我提桶跑路了

有一种打工人的羡慕&#xff0c;叫做“大厂”。 真是年少不知大厂香&#xff0c;错把青春插稻秧。 但是&#xff0c;在深圳有一群比大厂员工更庞大的群体&#xff0c;他们顶着大厂的“名”&#xff0c;做着大厂的工作&#xff0c;还可以享受大厂的伙食&#xff0c;却没有大厂…

基于龙芯+国产FPGA 的VPX以太网交换板设计(一)

“棱镜门”的曝光&#xff0c;暴露出我国的信息安全存在极大的安全隐患&#xff0c;作为信息传输 载体的网络设备&#xff0c;其国产化需求迫切&#xff0c;国产处理器、国产可编程逻辑器件、以太网交 换芯片等具有良好的应用前景&#xff0c;另一方面&#xff0c;宽带数据业务…

Java 对象深拷贝

需求 写一个java对象深拷贝工具&#xff0c;用以对象深拷贝。 分析 在 Java 中&#xff0c;变量间值的传递分为两种。对于 int,char,string,等基本数据类型进行值传递时&#xff0c;使用值传递。即原变量 a 的值与新变量 b 的值相等&#xff0c;且 a 与 b 拥有不同的内存地址…

HTML认知

HTML认知 文章目录HTML认知语法规范注释标签组成和关系标签的关系标签学习排版系列标签**标题标签****段落标签**换行标签水平线标签文本格式化标签媒体标签图片标签src 目标图片的路径alt 替换文本title 图片的标题width 宽度 / height 高度路径绝对路径相对路径&#xff08;常…

win10搭建android monkeyrunner自动化测试环境

本文记录一下monkeyrunner环境搭建遇到的各种坑&#xff0c;以免以后再次踩坑。首先要提一下巨坑&#xff0c;务必要安装java 8&#xff08;本文记录于2023.3&#xff09;&#xff0c;安装其他版本java&#xff0c;运行monneyrunner会有很多问题&#xff0c;见第七节。 一、安…

Threejs 教程1

threejs核心概念场景、照相机、对象、光、渲染器等1.1.场景Scene 场景是所有物体的容器&#xff0c;对应着显示生活中的三维世界&#xff0c;所有的可视化对象级相关的动作均发生在场景中。1.2.照相机Camera照相机是三维世界中的观察者&#xff0c;类似与眼睛。为了观察这个世界…

5款软件压力测试工具分享

一、什么是软件压力测试? 软件压力测试是一种基本的质量保证行为&#xff0c;它是每个重要软件测试工作的一部分。软件压力测试的基本思路很简单&#xff1a;不是在常规条件下运行手动或自动测试&#xff0c;而是在计算机数量较少或系统资源匮乏的条件下运行测试。通常要进行…

达梦: DmAPService可以手动开启,但是不能开机自启

在部署生产环境安全版数据库发现&#xff0c;DmAPService服务不能开机自启&#xff0c;这样会导致服务器重启后&#xff0c;部署的定时备份任务不能够成功备份数据。 为了在服务器重启后&#xff0c;不影响定时任务自动备份数据库&#xff0c;现将解决这个问题的办法总结如下&a…

《MySql学习》 MySQL的 加锁规则

MySQL加锁原则 两个原则 原则 1&#xff1a;加锁的基本单位是 next-key lock。next-key lock 是前开后闭区间&#xff08;区间锁和行锁&#xff09;。原则 2&#xff1a;查找过程中访问到的对象&#xff08;索引&#xff09;才会加锁。 两个优化 1.优化 1&#xff1a;索引上…

美团2面:如何保障 MySQL 和 Redis 数据一致性?这样答,让面试官爱到 死去活来

美团2面&#xff1a;如何保障 MySQL 和 Redis 的数据一致性&#xff1f; 说在前面 在尼恩的&#xff08;50&#xff09;读者社群中&#xff0c;经常遇到一个 非常、非常高频的一个面试题&#xff0c;但是很不好回答&#xff0c;类似如下&#xff1a; 如何保障 MySQL 和 Redis…

JavaSE学习进阶 day1_02 继承(面向对象第二大特性)

第三章 继承 &#xff08;面向对象的第二大特性&#xff09; 3.1 概述 3.1.1 引入 假如我们要定义如下类: 学生类,老师类和工人类&#xff0c;分析如下。 学生类 属性:姓名,年龄 行为:吃饭,睡觉 老师类 属性:姓名,年龄&#xff0c;薪水 行为:吃饭,睡觉&#xff0c;教书 班…

C++ List链表使用

1. list的介绍及使用1.1 list的介绍1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器&#xff0c;并且该容器可以前后双向迭代。2. list的底层是双向链表结构&#xff0c;双向链表中每个元素存储在互不相关的独立节点中&#xff0c;在节点中通过指针指向其前一个…

百度飞桨PaddleSpeech的简单使用

PaddleSpeech 是基于飞桨 PaddlePaddle 的语音方向的开源模型库&#xff0c;用于语音和音频中的各种关键任务的开发&#xff0c;包含大量基于深度学习前沿和有影响力的模型&#xff0c;一些典型的应用示例如下&#xff1a;语音识别、语音翻译 (英译中)、语音合成、标点恢复等。…