深入理解ThreadLocal原理

news2024/12/24 2:09:09

以下内容首发于我的个人网站,来这里看更舒适:https://riun.xyz/work/9898775
在这里插入图片描述


ThreadLocal是一种用于实现线程局部变量的机制,它允许每个线程有自己独立的变量,从而达到了线程数据隔离的目的。

基于JDK8

使用

通常在项目中是这样使用它的,创建线程变量工具类,然后用它去存取线程局部变量:

package com.example.demo.Utils;

/**
 * @author: HanXu
 * on 2021/11/17
 * Class description: 线程变量工具类
 * 在每一个线程中储存一个变量,以记录当前线程生命流程中的动作
 */
public class ThreadLocalUtil {

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


    /**
     * 获取值
     * @return 当前线程中存放的变量值
     */
    public static String getCurrentThreadVal() {
        return currentThreadLocal.get();
    }

    /**
     * set值
     * @param value 唯一
     */
    public static void putCurrentThreadVal(String value) {
        currentThreadLocal.set(value);
    }

    /**
     * 清空当前线程中的数据
     */
    public static void clear() {
        currentThreadLocal.remove();
    }
}

有了这个工具类,我们就能在线程执行时为每个线程放入不同的数据了,以下是一个小测试:

package com.example.demo.Utils;

import java.util.Random;
import java.util.concurrent.*;

/**
 * @author: HanXu
 * on 2024/6/26
 * Class description: 向10个线程中放入随机数,然后取出,查看放入取出是否一致
 */
public class Test {

    private static final int THREAD_NUM = 10;
    private static final CountDownLatch countDownLatch = new CountDownLatch(THREAD_NUM);
    private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(THREAD_NUM);

    public static void main(String[] args) throws InterruptedException {

        ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_NUM);
        for (int i = 0; i < THREAD_NUM; i++) {
            threadPool.execute(() -> {
                Random t = new Random();
                int num = t.nextInt(100);
                //等待所有线程都有任务再全部一起执行:在当前线程中放入100以内的随机数
                waitOtherThread();
                ThreadLocalUtil.putCurrentThreadVal(String.valueOf(num));
                System.out.println(Thread.currentThread().getName() + ",放入的数字:" + num);
                countDownLatch.countDown();
            });
        }

        countDownLatch.await();
        System.out.println();
        System.out.println();


        for (int i = 0; i < THREAD_NUM; i++) {
            threadPool.execute(() -> {
                //等待所有线程都有任务再全部一起执行:取出当前线程中存放的数字
                waitOtherThread();
                System.out.println(Thread.currentThread().getName() + ",取得数字:" + ThreadLocalUtil.getCurrentThreadVal());
                ThreadLocalUtil.clear();
            });
        }

        threadPool.shutdown();
        while (!threadPool.awaitTermination(3, TimeUnit.SECONDS)) {
            Thread.yield();
        }
        System.out.println("执行完毕!");
    }

    private static void waitOtherThread() {
        try {
            cyclicBarrier.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}

执行结果:

pool-1-thread-10,放入的数字:80
pool-1-thread-8,放入的数字:22
pool-1-thread-7,放入的数字:52
pool-1-thread-6,放入的数字:99
pool-1-thread-5,放入的数字:91
pool-1-thread-3,放入的数字:26
pool-1-thread-4,放入的数字:25
pool-1-thread-1,放入的数字:64
pool-1-thread-2,放入的数字:56
pool-1-thread-9,放入的数字:38


pool-1-thread-9,取得数字:38
pool-1-thread-10,取得数字:80
pool-1-thread-8,取得数字:22
pool-1-thread-7,取得数字:52
pool-1-thread-6,取得数字:99
pool-1-thread-5,取得数字:91
pool-1-thread-3,取得数字:26
pool-1-thread-4,取得数字:25
pool-1-thread-1,取得数字:64
pool-1-thread-2,取得数字:56
执行完毕!

原理

可以看到我们明明使用的是同一个ThreadLocal, 但作用到不同线程上就能隔离他们之间的数据。那ThreadLocal是如何做到线程间数据隔离的呢?

我们可以看下ThreadLocal.set的源码:

	public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

	void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

可以看到通过getMap(t),得到了一个类似Map的对象ThreadLocalMap,然后向map中存入数据时,是以当前对象this为key存入的。当前对象this就是当前ThreadLocal对象,我们使用的是同一个ThreadLocal,所以this是一样的。

那就肯定是map不同,再看下getMap(t)怎么获取的:

	ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

Thread t是当前线程,t.threadLocals就是获取的当前线程的threadLocals属性。

(在Thread类里有一个成员属性:ThreadLocal.ThreadLocalMap threadLocals = null;)

那它的原理就是每个Thread内部有一个ThreadLocalMap类型的属性变量threadLocals。然后每个线程执行ThreadLocal.set时,是向自己的threadLocals中储存数据。由于线程不同,所以threadLocals也就不同,达到了线程数据隔离的目的。而ThreadLocal只是用来操作当前线程中的ThreadLocalMap的工具类而已。所有的数据并没有放在ThreadLocal当中。

所以一句话总结就是:每个线程有自己的ThreadLocalMap,存取数据是从自己的ThreadLocalMap操作的。

细节

ThreadLocalMap

ThreadLocalMap是一个Map,所以它也是数组+链表结构;但是由于我们使用的时候是一个ThreadLocal对象,而存数据时是以当前ThreadLocal对象作为key的,所以这个Map中只会有一个索引位置被使用,且不会有链表形成。(所以它内部并没有链表的实现)

Entry

我们再来看看Entry(ThreadLocal -> static class ThreadLocalMap -> static class Entry):

		static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

很简单的k,v键值对构成的对象,但是它却继承了弱引用WeakReference,让自己的k变成了弱引用类型:super(k); 为什么是弱引用的key我们后文再说。

remove

一般我们使用set存值,get取值,当这个变量不再使用了,我们需要手动remote()清除掉,ThreadLocal.remote():

	public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

ThreadLocal.ThreadLocalMap.remote():

		private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            //获取ThreadLocal作为key在数组中的索引下标
            int i = key.threadLocalHashCode & (len-1);
            //虽然是循环,但是我们的使用方式数组中只会有一个索引有值
            for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    //使用的父类的clear方法,即:把key置为null
                    e.clear();
                    //把value置为null,通常也会把key为null的value置为null
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

我们在项目中使用ThreadLocal,在当前线程用完后,一定要手动remove(),这样在当前线程生命周期结束的时候,所有对象都会变为垃圾可回收。

内存泄露

如果我们在使用完ThreadLocal后,不手动remove(),若当前线程生命周期还未结束,那线程会一直持有ThreadLocalMap的引用,而ThreadLocalMap引用Entry,Entry引用了对应的key,value,而我们使用完了ThreadLocal,ThreadLocal已经没有了,就永远无法再获取这个Entry的key,value,这样就会造成了内存泄露:

所以正确的使用方法是:定义一个ThreadLocal,在线程运行时使用它,在使用完成之后执行remove()。

另外我们项目中一般都是用static final修饰ThreadLocal也是因为我们在项目运行期间只想要一个ThreadLocal对象,这样当发生一些不可预料的事情时,由于我们只有一个ThreadLocal对象,所以我们也能够操作之前这个位置的Entry内容,修改或删除它。

Why WeakReference?

通过上图我们可以看到,无论Entry的key使用强引用还是弱引用,如果没有remove(),那在线程生命周期没有结束时,都是会造成内存泄露的。那为什么要使用弱引用呢?

因为ThreadLocal的set / get /remove方法执行时,都会做一些额外的事:将key为null的Entry里的value也置为null:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //在这个方法内部
            map.set(this, value);
        else
            createMap(t, value);
    }

这样,假设我们在某个方法中定义了ThreadLocal,使用完未remove();而当我们下次在另一个方法中再定义一个ThreadLocal时,进行set / get /remove任意操作,都会将当前线程内ThreadLocalMap中key为null的Entry进行清除,将其value也置为null,将一个本该内存泄露的对象变为了可回收的垃圾。 这样相当于多了一层保障,从而减少内存泄露发生的可能:

可能有些同学注意到我上面强调了在线程生命周期内,这是因为如果线程生命周期结束了,则不管有没有remove(),对应的Entry一定会被作为垃圾回收。因为线程生命周期结束后,ThreadLocal和ThreadLocalMap都没有了,没有栈引用指向堆空间这些对象,所以他们都是垃圾可以被回收。

而在我们进行系统开发时,这点通常是不好控制的,因为有可能许多请求并发访问时,请求的线程都没有执行结束,所以如果我们不remove(),那一点点内存泄露就有可能导致内存溢出。

综上,ThreadLocal导致内存泄露的两个原因就是:

1、使用完没有remove()

2、使用完ThreadLocal后,线程生命周期并没有结束

所以要解决ThreadLocal的内存泄露问题,只需要满足任意一个即可:

1、一定要remove() 2、使用完ThreadLocal后,线程结束

但是第2点我们不好控制,所以一般都是使用第1点。

丢失ThreadLocal?

还有人可能在乎的点是:把key作为弱引用,发现即回收,若GC执行时发现了这个ThreadLocal那它不就被回收了吗?那我们程序执行不就出现NPE了吗?

其实不是的,因为我们还有一个自己定义的ThreadLocal threadLocal = new ThreadLocal()这个强引用在指向该ThreadLocal,所以在使用期间这个ThreadLocal是不会被垃圾回收的。

框架中的应用

Spring的事务管理器使用的ThreadLocal,SpringMVC的HttpSession、HttpServletRequest、HttpServletResponse都是放在ThreadLocal中的,以保证线程安全。

总结

因此,当我们想要隔离线程变量时,可以使用ThreadLocal,但是使用时要注意,一般定义一个static final的ThreadLocal,且使用完之后要一定记得remove();

另外ThreadLocal还有很多变种,比如InheritableThreadLocal和TransmittableThreadLocal。想要更多了解使用的可以看这篇文章:全链路追踪traceId

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

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

相关文章

仅1月出刊:计算机科学类知网检索普刊

【欧亚科睿学术】 Journal of Computer Science and Electrical Engineering 《计算机科学与电气工程杂志》是一份同行评审期刊&#xff0c;发表计算机科学和电气工程几个领域的原创研究文章和综述文章。 它由UPUBSCIENCE出版社出版。它支持开放获取政策&#xff0c;即让所有…

后台运行大师:HarmonyOS 3.0中如何轻松设置APP常驻后台

有不少人想要让某些常用的APP直接挂在后台&#xff0c;减少应用程序自动关闭的情况。这种需求&#xff0c;其实就是希望APP能够“保持在后台运行”。 本篇文章用14张图片、7大步骤&#xff0c;讲解手机如何将某个APP保持在后台运行。图片直接使用的是华为手机HarmonyOS 3.0的手…

Verilog开源项目——百兆以太网交换机(五)TCAM单元设计

Verilog开源项目——百兆以太网交换机&#xff08;五&#xff09;TCAM单元设计 &#x1f508;声明&#xff1a;未经作者允许&#xff0c;禁止转载 &#x1f603;博主主页&#xff1a;王_嘻嘻的CSDN主页 &#x1f511;全新原创以太网交换机项目&#xff0c;Blog内容将聚焦整体架…

iptables防火墙详解、相关命令示例

目录 Linux包过滤防火墙 包过滤的工作层次 iptables的链结构 规则链 默认包括5中规则链&#xff08;对数据包控制的时机&#xff09; iptables的表结构 规则表 默认包括4个规则表 数据包过滤的匹配流程 规则表之间的顺序 规则链之间的顺序 规则链内的匹配顺序 匹配…

加装德国进口高精度主轴 智能手机壳「高质量高效率」钻孔铣槽

在当前高度智能化的社会背景下&#xff0c;智能手机早已成为人们生活、工作的必备品&#xff0c;智能手机壳作市场需求量巨大。智能手机壳的加工过程涉及多个环节&#xff0c;包括钻孔和铣槽等。钻孔要求精度高、孔位准确&#xff0c;而铣槽则需要保证槽位规整、深度适宜。这些…

stm32学习笔记---USART串口外设(理论部分)

目录 USART简介 USART的框图 串口的引脚 USART的基本结构 数据帧 起始位侦测 数据采样 波特率发生器 USD转串口模块的原理图 声明&#xff1a;本专栏是本人跟着B站江科大的视频的学习过程中记录下来的笔记&#xff0c;我之所以记录下来是为了方便自己日后复习。如果你…

python实现简单的三维建模学习记录

课程来源与蓝桥云课Python 实现三维建模工具_Python - 蓝桥云课和500 Lines or LessA 3D Modeller 说明 个人估计这是一个值得花一个礼拜左右时间去琢磨的一个小项目。上述网址中的代码直接拿来不一定能跑&#xff0c;需要后期自己去修改甚至在上面继续优化&#xff0c;会在其…

【Gin】项目搭建 一

环境准备 首先确保自己电脑安装了Golang 开始项目 1、初始化项目 mkdir gin-hello; # 创建文件夹 cd gin-hello; # 需要到刚创建的文件夹里操作 go mod init goserver; # 初始化项目&#xff0c;项目名称&#xff1a;goserver go get -u github.com/gin-gonic/gin; # 下载…

【LeetCode】十、二分查找法:寻找峰值 + 二维矩阵的搜索

文章目录 1、二分查找法 Binary Search2、leetcode704&#xff1a;二分查找3、leetcode35&#xff1a;搜索插入位置4、leetcode162&#xff1a;寻找峰值5、leetcode74&#xff1a;搜索二维矩阵 1、二分查找法 Binary Search 找一个数&#xff0c;有序的情况下&#xff0c;直接…

国产压缩包工具——JlmPackCore SDK说明(三)——JlmPack_Unpack函数说明

一、JlmPack_Unpack函数说明 JlmPack_Unpack函数是解压jlm文件的核心函数&#xff0c;但是在加密状态下&#xff0c;必须有正确的密码才能解密&#xff0c;该函数具有一定的权限管控条件&#xff0c;部分也需要开发者通过上层系统进行控制。库函数名&#xff1a; JLMPACK_API …

做了个三相电量采集器开源出来,可以方便监测家里用电情况

做了个三相电能采集器&#xff0c;可以测3相的电流、电压、功率、功率因数、用电量&#xff0c;数据上传到HomeAssistant&#xff0c;方便观察家里用电量和实时用电功率。 使用3个pzem004t电参数传感器测量&#xff0c;通过串口与ESP32-C3通信&#xff0c;然后通过WiFi上传至H…

WordPress网站如何做超级菜单(Mega Menu)?

大多数的网站菜单都是像以下这种条状的形式&#xff1a; 这种形式的是比较中规中矩的&#xff0c;大多数网站都在用的。当然还有另外一种菜单的表现形式&#xff0c;我们通常叫做“超级菜单”简称Mega Menu。网站的超级菜单&#xff08;Mega Menu&#xff09;是一种扩展的菜单&…

使用ElementUI组件库

引入ElementUI组件库 1.安装插件 npm i element-ui -S 2.引入组件库 import ElementUI from element-ui; 3.引入全部样式 import element-ui/lib/theme-chalk/index.css; 4.使用 Vue.use(ElementUI); 5.在官网寻找所需样式 饿了么组件官网 我这里以button为例 6.在组件中使用…

数组-移除元素

移除元素 移除元素&#xff08;leetcode27&#xff09; var removeElement function(nums, val) {const n nums.length;let left 0;for (let right 0; right < n; right) {if (nums[right] ! val) {nums[left] nums[right];left;}}return left; };删除有序数组中的重复…

GPT-4o不仅能写代码,还能自查Bug,程序员替代进程再进一步!

目录 1 CriticGPT 01 综合性&#xff08;Comprehensiveness&#xff09;&#xff1a; 02 幻觉问题&#xff08;Hallucinates a problem&#xff09;&#xff1a; 2 其他 CriticGPT 案例 随着人工智能&#xff08;AI&#xff09;技术不断进步&#xff0c;AI在编程领域的应用…

hive中cast()函数

CAST函数用于将某种数据类型的表达式显式转换为另一种数据类型。CAST()函数的参数是一个表达式&#xff0c;它包括用AS关键字分隔的源值和目标数据类型。 语法&#xff1a;CAST (expression AS data_type) expression&#xff1a;任何有效的SQServer表达式。 AS&#xff1a;用…

ATFX汇市:欧元区CPI与失业率数据同时发布,欧元或迎剧烈波动

ATFX汇市&#xff1a;CPI数据是中央银行决策货币政策的主要依据&#xff0c;失业率数据是中央银行判断劳动力市场健康状况的核心指标。欧元区的CPI和失业率数据将在今日17:00同时发布&#xff0c;在欧央行6月6日降息一次的背景下&#xff0c;两项数据将显著影响国际市场对欧央行…

2024 年江西省研究生数学建模竞赛题目 A题交通信号灯管理---完整文章分享(仅供学习)

问题&#xff1a; 交通信号灯是指挥车辆通行的重要标志&#xff0c;由红灯、绿灯、黄灯组成。红灯停、绿灯行&#xff0c;而黄灯则起到警示作用。交通信号灯分为机动车信号灯、非机动车信号灯、人行横道信号 灯、方向指示灯等。一般情况下&#xff0c;十字路口有东西向和南北向…

HR人才测评,如何考察想象力?

什么是想象力&#xff1f; 想象力是指&#xff0c;人们通过在已有物质的基础上&#xff0c;通过大脑想象、加工、创造出新事物的能力&#xff0c;举一个非常简单的例子&#xff0c;在提到鸟这种生活的时候&#xff0c;大家会联想到各种各样不同鸟的品种。 在企业招聘中常常应…

3.1 数据结构-线性表

上午10-12分的选择题&#xff0c;下午15分的大题 大纲 线性结构 顺序存储和链式存储区别 单链表的插入和删除 真题 线性结构 - 栈和队列 真题 串