ThreadLocal解惑

news2024/9/24 23:24:41

目录

1、ThreadLocal是什么?

2、ThreadLocal实现原理

3、设置线程变量的2种方式

4、关于ThreadLocal的内存泄漏问题

5、使用过程中的注意事项和误区


1、ThreadLocal是什么?

    比较书面的回答:
类如其名,线程本地变量。当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程。这句话没问题,但容易被人误解,会被误以为:任意变量用ThreadLocal维护都是线程隔离的。后面会解答这个问题。

2、ThreadLocal实现原理

       每个线程(Thread)中都有一个 ThreadLocalMap容器,通过ThreadLocal可以存放和读取Thread中的ThreadLocalMap。每个Thread对象之间是隔离的,Thread对象中的ThreadLocalMap容器自然也是隔离的。
通俗点来说:可以把ThreadLocal看着是一个工具类,通过这个工具类的get、set、remove等方法可以操作各自线程对象中的ThreadLocalMap,实现互不干扰。

要探究原里,就离不开两个类:Thread和ThreadLocal,先分别看一下这两个类,为了方便理解,这里就简要介绍核心的概念和源码,更细节的东西查看源码就行,源码不多也很简单。

1)关于Thread类

如下代码判断,代码中的任意位置我们都可以通过 Thread.currentThread() 来获取当前线程对象,即可以获得当前线程的名称、id等等属性;但是无法直接获取到Thread中的ThreadLocalMap。

    public static void main(String[] args) {
        // 获取当前线程-主线程
        Thread mainThread = Thread.currentThread();
        System.out.println("main thread id: " + mainThread.getId());
        System.out.println("main thread name: " + mainThread.getName());

        System.out.println("------------------------------");

        // 自定义线程 1
        Thread thread1 = new Thread(()-> {
            // 获取当前线程
            Thread th = Thread.currentThread();
            System.out.println("thread id: " + th.getId());
            System.out.println("thread name: " + th.getName());
        });
        // 设置线程名称
        thread1.setName("MyThread-1");
        thread1.start();
    }

执行结果:

看Thread类的源码,里面有一个属性ThreadLocalMap,该Map就是用来存储各线程独立变量的。

2) 关于ThreadLocal类
    前面说了,可以把它看着是一个工具类,通过这个工具类的get、set、remove等方法可以操作各自线程对象中的ThreadLocalMap。

简单看一下ThreadLocal类的源码

Thread类中的ThreadLocalMap属性是ThreadLocal类中的内部类

从源码可以看出,ThreadLocal类中有内部类ThreadLocalMap,ThreadLocalMap中有内部类Entry,Entry类有两个属性,k和v。ThreadLocalMap是用的Entry数组来存储数据(Entry对象)。

使用ThreadLocal的set方法添加一个变量,下面通过代码来看一下这个流程

    public static void main(String[] args) {
        ThreadLocal<User> threadLocal = new ThreadLocal<>();
        // 创建线程1
        Thread thread1 = new Thread(()->{
            User user = new User("user1");
            // 添加当前线程的变量,和其他线程隔离
            threadLocal.set(user);
        });
        // 设置名称、启动
        thread1.setName("thread1");
        thread1.start();
    }

ThreadLocal.set()方法的源码:

    public void set(T value) {
        // 得到当前线程对象
        Thread t = Thread.currentThread();
        // 得到当前线程对象中的Map
        ThreadLocalMap map = getMap(t);
        // Map不为空就把值添加进去,this就是ThreadLocal对象,如果为空就创建一个Map
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

ThreadLocal.get()也是类似的道理,先拿到当前线程对象,再拿到当前线程对象中的ThreadLocalMap,再从中取值。

最终还是应了开头那句话,可以把ThreadLocal看着一个工具类,可以用他来往当前线程中存储和获取值。

3、设置线程变量的2种方式
 

1)创建ThreadLocal对象时设置变量

创建ThreadLocal对象时设置初始化值,通过执行结果可以看出,每个线程在第一次调用get方法获取值的时候都会执行该段代码初始化变量,也就是每个线程得到的是一个新的对象,最终都存储到自己线程Thread的ThreadLocalMap容器中,不是同一个对象,也不是同一个存储容器,当然是隔离的。
@Data
class User {
    private String userName;
    public User() {}
    public User(String userName) {
        System.out.println("init user...");
        this.userName = userName;
    }
}

public class Test {
    public static void main(String[] args) throws InterruptedException {
        // 创建ThreadLocal,并设置初始化值
        ThreadLocal<User> threadLocal = ThreadLocal.withInitial(()-> {
            // 每个线程在不执行set方法设置变量的情况下,第一次调用get方法获取值的时候执行该段代码,初始化变量,也就是每个线程得到的是一个新的对象
            User user = new User("user1");
            return user;
        });

        // 创建线程1
        Thread thread1 = new Thread(()->{
            System.out.println("thread1 .......");
            threadLocal.get().setUserName("T1-user");
            System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
            // 用完后清除,避免内存泄漏
            threadLocal.remove();
        });
        // 设置名称、启动
        thread1.setName("thread1");
        thread1.start();

        Thread.sleep(1000);

        // 创建线程2
        Thread thread2 = new Thread(()->{
            System.out.println("thread2 .......");
            System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
            // 用完后清除,避免内存泄漏
            threadLocal.remove();
        });
        thread2.setName("thread2");
        thread2.start();
    }
}


2)通过set()方法设置变量

创建threadLocal对象,不设置初始化值,在各自的线程中通过set方法设置变量。

@Data
class User {
    private String userName;
    public User() {}
    public User(String userName) {
        System.out.println("init user...");
        this.userName = userName;
    }
}

public class Test {

    public static void main(String[] args) throws InterruptedException {
        // 创建threadLocal对象,不设置初始化值
        ThreadLocal<User> threadLocal = new ThreadLocal<>();

        // 创建线程1
        Thread thread1 = new Thread(()->{
            User user = new User("user1");
            // 添加当前线程的变量,和其他线程隔离
            threadLocal.set(user);
            System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
            // 用完后清除,避免内存泄漏
            threadLocal.remove();
        });


        // 创建线程2
        Thread thread2 = new Thread(()->{
            User user = new User("user2");
            // 添加当前线程的变量,和其他线程隔离
            threadLocal.set(user);
            System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
            // 用完后清除,避免内存泄漏
            threadLocal.remove();
        });

        // 设置名称、启动
        thread1.setName("thread1");
        thread1.start();

        Thread.sleep(1000);

        thread2.setName("thread2");
        thread2.start();
    }
}

执行结果:

4、关于ThreadLocal的内存泄漏问题

提到ThreadLocal,肯定都会想到内存泄漏,当ThreadLocalMap的Entry中的key为null,而value不为null时,该value就永远不能被访问到,就是一个无用的对象,按理来说应该被回收,而根据可达性分析导致在垃圾回收的时候进行可达性分析的时候,如果当前线程没有结束,当前线程持有ThreadLocalMap,ThreadLocalMap持有Entry对象,Entry对象包含value,value可达从而不会被回收掉,这样就存在了内存泄漏。

1)话接上面,为什么ThreadLocalMap的Entry中的key会为null呢?

因为Entry中的key是弱引用,在垃圾回收的时候,如果key没有被其他对象引用,也就是说后续代码中不会再被用到,他就会被回收,最终Entry中的key为null。原来ThreadLocal对象在这里被引用,现在key为空,ThreadLocal在这里就没有被引用,如果其他地方也没有引用ThreadLocal对象,ThreadLocal对象就可以被回收,释放内存。

在使用完ThreadLocal后调用其remove方法,就可以清除不被使用的变量,避免内存泄漏。

        Thread thread2 = new Thread(()->{
            User user = new User("user2");
            // 添加当前线程的变量,和其他线程隔离
            threadLocal.set(user);
            System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
            // 用完后清除,避免内存泄漏
            threadLocal.remove();
        });

在ThreadLocal中,调用get、set、remove方法都会清除key为空的value,避免内存泄漏。

通过源码,可以追踪到 expungeStaleEntry 方法,该方法会清空key为空的value。



2)既然key是弱引用,GC回收会影响ThreadLocal的正常工作吗?

不会,因为有ThreadLocal变量引用着它,也就是说后面还会用到他,是不会被GC回收的,执行一段代码一探究竟。
 

        Thread thread1 = new Thread(()->{
            // 设置变量
            threadLocal.set(new User("thread1-user"));
            // 输出变量
            System.out.println(threadLocal.get().getUserName());
            System.gc(); //垃圾回收
            System.out.println("gc...gc");
            // 输出变量
            System.out.println(threadLocal.get().getUserName());
        });

执行结果:

可以看到,如果后续还会用到,是不会被回收的,不然问题就大了:“上一秒刚设置的变量,下一秒获取的时候就没了?”。



5、使用过程中的注意事项和误区

1)ThreadLocal与线程池

一般web容器,如tomcat就使用了线程池,或者我们自定义的线程池,线程池中的线程是存在复用情况的。如果我们在当前线程中使用ThreadLocal设置了一个变量,】并且没有执行remove方法,当前线程执行结束后,线程还在线程池中存在,线程并没有被销毁,下一个请求过来就会使用线程池中的线程,就会拿到上一个请求在线程中设置的变量。所以使用玩后一定要调用ThreadLocal的remove方法。

2)错误的理解导致使用方法
很多人看见这句话:“用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程”,就会理解为变量或者内存的完全隔离,就会出现错误的用法,例如:

错误方式一

@Data
class User {
    private String userName;
    public User() {}
    public User(String userName) {
        System.out.println("init user...");
        this.userName = userName;
    }
}

public class Test {
    // 这是一个公共的变量
   public static User user = new User("user1");

    public static void main(String[] args) throws InterruptedException {
        ThreadLocal<User> threadLocal = new ThreadLocal<>();
        // 创建线程1
        Thread thread1 = new Thread(()->{
            // 设置变量
            threadLocal.set(user);
            // 线程1改变了user对象的值
            threadLocal.get().setUserName("thread1-user");
            System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
        });

        // 创建线程2
        Thread thread2 = new Thread(()->{
            // 设置变量
            threadLocal.set(user);
            // 可拿到线程1中改变的user对象的值
            System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
        });

        // 设置名称、启动
        thread1.setName("thread1");
        thread1.start();

        Thread.sleep(1000);

        thread2.setName("thread2");
        thread2.start();
    }
}

错误方式二

@Data
class User {
    private String userName;
    public User() {}
    public User(String userName) {
        System.out.println("init user...");
        this.userName = userName;
    }
}

public class Test {
    // 这是一个公共的变量
   public static User user = new User("user1");

    public static void main(String[] args) throws InterruptedException {
        ThreadLocal<User> threadLocal = ThreadLocal.withInitial(()-> user);
        // 创建线程1
        Thread thread1 = new Thread(()->{
            // 线程1改变了user对象的值
            threadLocal.get().setUserName("thread1-user");
            System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
        });

        // 创建线程2
        Thread thread2 = new Thread(()->{
            // 可拿到线程1中改变的user对象的值
            System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
        });

        // 设置名称、启动
        thread1.setName("thread1");
        thread1.start();

        Thread.sleep(1000);

        thread2.setName("thread2");
        thread2.start();
    }
}

执行结果:

是不是意外,不是说变量在线程之间是隔离的吗?怎么线程1改了user对象的值,线程二中也随之改变了呢?

因为ThreadLocal设置变量(对象)的时候,并不是拷贝一份新的变量(对象),而是直接赋值对象的引用,如果这个变量(对象)是一个公共变量(对象),那么各线程的ThreadLocalMap中的key所指向的其实还是同一个对象,并没有隔离。

代码说明:

public class Test {
    // 这是一个公共的变量
   public static User user = new User("user1");
   
   // ***** 线程不隔离
   // 不能到达user对象在各线程中互相隔离的效果, 因为user本身就是公共变量
   public static ThreadLocal<User> threadLocal = ThreadLocal.withInitial(()-> user);
   
    // ***** 线程隔离
   public static ThreadLocal<User> threadLocal2 = ThreadLocal.withInitial(()-> {
        // 每个线程在不执行set方法设置变量的情况下,第一次执行get方法的时候都会执行本段代码,创建新的对象,现场之间使用的就不是同一个user对象
        User user = new User("user1");
        return user;
    });
}

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

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

相关文章

防爆巡检机器人:工业安全领域的璀璨明星

在当今快速发展的工业领域&#xff0c;安全与效率是企业追求的双核动力。特别是在石油、化工、钢铁冶金、燃气等高风险、高爆炸性的行业中&#xff0c;如何确保生产环境的绝对安全&#xff0c;同时提升巡检效率&#xff0c;成为了企业亟需解决的重大课题。正是在这样的背景下&a…

leetcode 438 找到字符串中所有字母异位词

leetcode 438 找到字符串中所有字母异位词 正文 正文 本题和 leetcode 49 字母异位分词 有些类似&#xff0c;只是 49 题中要求我们找出所有的异位词并进行存储&#xff0c;而本题我们只需要找出异位词对应的索引值。因此&#xff0c;我们无需用到字典&#xff0c;只需使用列表…

理解线程 ID 和 LWP

序言 在不同的系统中&#xff0c;为了更好地管理用户可能会采取不同的编号。比如在学校的教务系统中&#xff0c;管理学生使用的是学号&#xff1b;但是在住宿系统中&#xff0c;为了更加方便的获取一个学生的寝室信息&#xff0c;可能会采取结合你是哪一栋&#xff0c;哪一层&…

MindSearch 部署的到 Hugging Face Space

和原有的CPU版本相比区别是把internstudio换成了github codespace。 随着硅基流动提供了免费的 InternLM2.5-7B-Chat 服务&#xff08;免费的 InternLM2.5-7B-Chat 真的很香&#xff09;&#xff0c;MindSearch 的部署与使用也就迎来了纯 CPU 版本&#xff0c;进一步降低了部署…

【Windows】深度学习环境部署

引言 1 Windows环境准备 1.1 VSCode Visual Studio Code&#xff08;简称 VSCode&#xff09;是一款由微软开发的开源代码编辑器。它非常受开发者欢迎&#xff0c;因为它功能强大、扩展性好&#xff0c;并且支持多种编程语言。VSCode 尤其适合 Python 开发&#xff0c;特别是…

WEB渗透免杀篇-Pezor免杀

往期文章 WEB渗透免杀篇-免杀工具全集-CSDN博客 WEB渗透免杀篇-加载器免杀-CSDN博客 WEB渗透免杀篇-分块免杀-CSDN博客 WEB渗透免杀篇-Powershell免杀-CSDN博客 WEB渗透免杀篇-Python源码免杀-CSDN博客 WEB渗透免杀篇-C#源码免杀-CSDN博客 WEB渗透免杀篇-MSFshellcode免杀…

文心一言 VS 讯飞星火 VS chatgpt (331)-- 算法导论22.5 7题

七、给定有向图 G ( V &#xff0c; E ) G(V&#xff0c;E) G(V&#xff0c;E)&#xff0c;如果对于所有结点对 u , v ∈ V u,v∈V u,v∈V,我们有 u → v u→v u→v或 v → u v→u v→u&#xff0c;则 G G G是半连通的。请给出一个有效的算法来判断图 G G G是否是半连通的。证…

根据需求、质量属性描述和架构特性开发一套公路桥梁在线管理系统

目录 案例 【题目】 【问题 1】(12 分) 【问题 2】(13 分) 答案 【问题 1】答案 【问题 2】答案 相关推荐 案例 阅读以下关于软件架构评估的叙述&#xff0c;在答题纸上回答问题 1 和问题 2。 【题目】 某单位为了建设健全的公路桥梁养护管理档案&#xff0c;拟开发一套公…

若依框架搭建

一、后端启动 1、git克隆下载前后端分离版本 RuoYi-Vue: &#x1f389; 基于SpringBoot&#xff0c;Spring Security&#xff0c;JWT&#xff0c;Vue & Element 的前后端分离权限管理系统&#xff0c;同时提供了 Vue3 的版本 (gitee.com) 2、初始化项目 到springboot后如…

Excel中的“LOOKUP”:熟识四个LOOKUP,可以让数据“查找”得心应手

熟识四个lookup&#xff0c;可以让数据“查找”得心应手。 (笔记模板由python脚本于2024年08月23日 19:27:16创建&#xff0c;本篇笔记适合喜欢用Excel处理数据的coder翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff1a;https://www.python.org/ Free&#xff1a;大咖…

入门 PyQt6 看过来(项目)29 在线购物-销售分析

销售分析部分包含按月分析的簇状图和按类别分析的饼图&#xff0c;如下&#xff1a; ​ 1 页面设计 设计该页面其实很简单&#xff0c;说白了就是两个也切tab类以及饼图和簇状图。打开QTDesiger&#xff0c;按下图添加控件&#xff0c;并重命名如下&#xff1a; ​ 2 按类…

计算机视觉与视觉大模型对板书检测效果对比

文章目录 计算机视觉火山引擎ocr阿里云ocr 视觉大模型GPT4kimi通义千问chatGLM百度 全部正确某开源模型&#xff0c;效果不佳 计算机视觉 火山引擎ocr 阿里云ocr 视觉大模型 GPT4 kimi 通义千问 chatGLM 百度 全部正确 某开源模型&#xff0c;效果不佳

基于springboot的养老院管理系统的设计与实现 (含源码+sql+视频导入教程)

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1 、功能描述 基于springboot的养老院管理系统拥有多种角色账号&#xff1a;管理员和用户 管理员&#xff1a;管理员管理、用户管理、健康管理、病例方案管理、药品管理、餐饮管理、外出管理、入住管理…

汇编

汇编指令 随机数mov指令mov指令ldr指令&#xff08;伪指令&#xff09;add指令sub指令bic指令orr指令b指令cmp指令stmfd指令ldmfd指令import栈汇编指令的s后缀 随机数 1.如果某个数的数值范围是0~255之间&#xff0c;那么这个数一定是立即数&#xff1b; 2.把某个数展开成2进制…

树与图的宽度优先遍历

大致思想请参照添加链接描述该篇博客 主要地方的差异就是&#xff1a; 宽度优先遍历就是一层一层的搜索 图中数的层次题目 给定一个 n个点 m条边的有向图&#xff0c;图中可能存在重边和自环。 所有边的长度都是 1&#xff0c;点的编号为 1∼n。 请你求出 1号点到 n号点的…

C++风格指南 2、作用域

2.1. 命名空间 这段文字的关键内容概括如下&#xff1a; 1. 命名空间的使用&#xff1a;除了少数特殊情况外&#xff0c;代码应在命名空间内&#xff0c;命名空间名称应唯一&#xff0c;包含项目名和可选的文件路径。 2. 禁止使用&#xff1a; - using 指令引入整个命名空…

实验17:直流电机实验

硬件接线图; 我这里实现的是&#xff1a;转5s&#xff0c;停5s&#xff0c;循环 main.c #include<reg52.h>typedef unsigned int u16; typedef unsigned char u8;sbit ZLP1^0;void delay_10us(u16 n) {while(n--); }void delay_ms(u16 ms) {u16 i,j;for(ims;i>0;i--…

Python中8个让你成为调试高手的技巧

文末赠免费精品编程资料~~ 调试技能是每一位开发者不可或缺的利器。它不仅能帮你迅速定位并解决代码中的bug&#xff0c;还能提升你的编程效率&#xff0c;让你的代码更加健壮。今天&#xff0c;我们就来揭秘10个让你从新手进阶为调试高手的秘诀。 1. 使用print()函数——基础…

AIoTedge边缘物联网平台发布,更低的价格,更强大的功能

AIoTedge是一个创新的AI边缘计算平台&#xff0c;专为满足现代物联网&#xff08;IoT&#xff09;需求而设计。它采用了边云协同的架构&#xff0c;能够实现多点部署&#xff0c;并与IoT云平台无缝配合&#xff0c;提供分布式的AIoT处理能力。这种设计特别适合需要AI云端训练和…

高效率伪原创检测,6款工具为你轻松搞定

在内容创作领域&#xff0c;原创性是衡量作品价值的重要标准之一。然而&#xff0c;创作高质量的原创内容不仅需要灵感和创意&#xff0c;还需要大量的时间和精力。为了提高效率&#xff0c;许多创作者和编辑开始寻求伪原创检测工具的帮助&#xff0c;以确保他们的作品在保持独…