线程私有:ThreadLocal

ThreadLocal 的本质:线程的“私有储物柜” 摘要:ThreadLocal是Java中用于实现线程封闭的机制,每个线程拥有独立的变量副本。其核心原理是通过Thread内部的ThreadLocalMap存储数据,key为弱引用的ThreadLocal实例。主要风险在于线程池复用场景:1)内存泄漏(因value强引用未被清理);2)上下文污染(线程复用导致数据错乱)。关键解决方案是必须调用remove()清理,或通过任务包装传递上下文。典型应用包括用户身份传递、链路追踪和事务管理。正确处理ThreadLocal与线程池的关系是保证线程安全的关键。 阅读详情

不知道大家有没有接触过ThreadLocal这个类,我还清楚的记得,自己当初第一次使用到这个类是缓存用户信息。刚开始学习java时,那个时候并没有想到什么分布式啊,单点登录(SSO),就使用ThreadLocal来保存用户一些全局信息,获取非常方便。那时,我只知道它是线程隔离的,非常好用,渐渐的,当它的身影出现越多的时候,对它的了解也就加深了一点,现在就让我们来领略一下ThreadLocal的魅力吧。

初始ThreadLocal

从类名来看,可以理解为线程本地化变量,那么它肯定就是线程安全的。在我们之前认识中,如果我们需要获得一个线程安全类,要么使用同步操作,要么就是加锁,或者使用更加高级的CAS操作。在这里,我想告诉大家,我们可以在不使用它们的情况下,获得一个线程安全的类,不信?大家都知道SimpleDateFormat是一个线程不安全的类,如果谁在并发环境中共享它,我相信大家都会鄙视的。咳咳,我当初就是被鄙视的一员,当然现在不会犯这么低级的错误了。SimpleDateFormat错误的用法:

public class SimpleDateFormatDemo {
    private final static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 50; i++) {
            int j = i;
            executorService.execute(() -> {
                Date parse = null;
                try {
                    parse = SimpleDateFormatDemo.format.parse("2019-01-" + (j % 30));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
                System.out.println(parse);
            });
        }
        executorService.shutdown();
    }
}

大家别笑(^_^),当初我就写过这样的代码,虽然和此代码不同,但是会引起的问题是一样的。大家运行这段代码,应该都会遇到类似的错误:

java.lang.NumberFormatException: For input string: ""
java.lang.NumberFormatException: For input string: "2424E"
java.lang.NumberFormatException: multiple points

这是因为SimpeDateFormat不是线程安全的,当我们在多个线程共享此变量时,各个线程读取到不一致内部状态。大家试想一下,多个线程同时使用SimpleDateFormat,假如某个线程刚设置了当前所要解析的日期,时间片用完暂停执行,另外一个线程开始执行,也设置了要解析的日期并暂停,前一个线程继续执行就会读取到不一致的结果,综上所述,SimpleDateFormat在多线程下工作是不友好的。

那么,应该怎么办呢?我们首先会想到的既然它不是线程安全的,那么我们就不共享它,在每次使用它的时候都new一个新的对象出来,这样不就可以了。大家试想一下,SimpleDateFormat是使用非常频繁的一个类,若我们每次都去创建,对内存和GC都不友好,此方法不可行。根据传统线程安全的做法,要想一个线程不安全的对象或者方法变成线程安全的,无非就是采取同步操作或者加锁,但是我们都知道采用这些方式会降低系统并发,传统方法是采用对象共享,串行化方式运行,是以时间换空间的思想,并且它的实现方式复杂,不易于理解。现在,有一种全新的方式,采用ThreadLocal,它是线程本地化变量,线程独享,多线程之间维护各自变量互不影响。ThreadLocal采用空间换时间的方式,对象独享化,有利于提高系统并发。SimpleDateFormat在多线程下安全使用:

public final class ThreadLocalDemo {
    private static ThreadLocal<SimpleDateFormat> threadLocal=new ThreadLocal<>();

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 50; i++) {
            Runnable runnable = () -> {
                if(threadLocal.get()==null){
                    System.out.println(Thread.currentThread().getId()+"初始化SimpleDateFormat");
                    threadLocal.set(new SimpleDateFormat("yyyy-MM-dd"));
                }
                try {
                    Date parse = threadLocal.get().parse("2019-01-07");
                    System.out.println(parse);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            };
            executorService.execute(runnable);
        }
        executorService.shutdown();
    }
}

这里,我们使用ThreadLocal包装SimpelDateFormat,然后在多线程中使用它,我们来看看结果如何。执行如下(截取部分输出):

16初始化SimpleDateFormat
17初始化SimpleDateFormat
15初始化SimpleDateFormat
14初始化SimpleDateFormat
20初始化SimpleDateFormat
21初始化SimpleDateFormat
18初始化SimpleDateFormat
22初始化SimpleDateFormat
23初始化SimpleDateFormat
19初始化SimpleDateFormat
Mon Jan 07 00:00:00 SGT 2019
Mon Jan 07 00:00:00 SGT 2019
Mon Jan 07 00:00:00 SGT 2019
Mon Jan 07 00:00:00 SGT 2019
Mon Jan 07 00:00:00 SGT 2019
Mon Jan 07 00:00:00 SGT 2019
Mon Jan 07 00:00:00 SGT 2019
Mon Jan 07 00:00:00 SGT 2019
Mon Jan 07 00:00:00 SGT 2019
Mon Jan 07 00:00:00 SGT 2019
Mon Jan 07 00:00:00 SGT 2019
Mon Jan 07 00:00:00 SGT 2019
Mon Jan 07 00:00:00 SGT 2019

大家可以发现,我们线程池中定义了10个线程,循环50次执行,SimpelDateFormat只初始化10次,这说明什么?这说明10个线程间使用的对象不是同一个,它们是线程私有的,互不影响,并且下一次线程执行的时候,发现对象已经初始化之后会直接获取执行。它是怎么做到的呢?请看下面这三个方法:

public void set(T value);//该方法是ThreadLocal用来保存局部变量
public T get();//该方法用来获取ThreadLocal保存的局部变量
public void remove();//该方法用来移除ThreadLocal保存的局部变量

下面我们来分别分析这三个方法:

set()

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //根据当前线程获取ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //map不为空,当前ThreadLocal作为key,插入value到map中
        map.set(this, value);
    else
        //否则创建map并把对象保存
        createMap(t, value);
}

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

关于threadLocals,Thread.java中定义如下:

ThreadLocal.ThreadLocalMap threadLocals = null;

set()方法,首先获取当前线程,然后根据当前线程获取ThreadLocalMap(与平时使用的Map没有关系,它是ThreadLocal的静态内部类)数据存储结构。如果map不为空,当前ThreadLocal对象作为key,把value插入到map中;否则根据当前线程创建ThreadLocalMap,然后把value保存进去。其中ThreadLocalMap是Thread的成员变量,说明ThreadLocalMap是线程独享的,只能根据当前线程去获取。

get()

public T get() {
    //获取当前线程
    Thread t = Thread.currentThread();
    //根据当前线程获取ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //根据当前ThreadLocal对象获取Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            //获取结果返回
            T result = (T)e.value;
            return result;
        }
    }
    //若map为空,初始化map,插入null值
    return setInitialValue();
}
//数据保存在Entry中,key为ThreadLocal对象
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

get()方法用户获取线程局部变量,首先根据当前线程获取ThreadLocalMap,若map不为空,根据ThreadLocal对象获取Entry对象,然后获取value,若map为空则初始化map并赋null值。若未调用set()方法,先调用get(),返回null。这里的Entry被定义为弱引用,弱引用就是比强引用弱很多的引用,当jvm进行垃圾回收时,发现弱引用,会立即进行回收。

remove()

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
    //根据当前ThreadLocal对象移除数据
        m.remove(this);
}

注意:ThreadLocalMap作为Thread的成员变量,当线程使用完未被销毁,ThreadLocalMap中保存的局部变量还是会一直存在,这就会引发内存泄漏。特别是在和线程池结合使用时,假如你现在定义一个线程池,为线程存储局部变量,当线程使用结束,并未把线程进行销毁而是直接归还到池中,内存泄漏就无可避免,所以当使用完毕最好使用remove()清除局部变量或者使用threadLocal=null。

总结

ThreadLocal和传统做法不一样的地方就在于,它是以空间换时间的方式,对象独享,实现线程安全。它不足的地方就在于会创建变量副本,增大内存消耗,但它能消除同步和锁产生的性能消耗,提高系统并发,所以它还是有自己的独到之处。

在这里多说一句,今天在公司关于SimpleDateFormat遇到这样一个问题,上游系统传到下游系统2019-02-30的字符串日期,然后使用SimpleDateFormat解析变成了2019-03-02号,导致还款计划错误。本身2019-02-30这个日期就是错误的,如果日期不合法,解析应该抛出异常而不是自动转换。后面发现SimpleDateFormat有这样一个方法setLenient(boolean b),当传入值为false时,进行严格校验,不合法抛出异常,为true时,则进行不严格校验并自动转换。大家若对日期有严格的校验,不妨试试这个方法。

 

 

ThreadLocal详解:线程私有变量的正确使用姿势 张三往自己口袋里放钱,不会影响李四的口袋李四从自己口袋里拿钱,也不会拿到张三的钱ThreadLocal就像给每个线程发了一个专属保险箱,让多线程编程变得简单安全。 阅读详情

相关推荐

C# 进程间 双内存共享 高帧率图像传输

演示了两个进程之间通过 双内存共享 的方式, 来实时处理图像,并以高帧率(40~60FPS)传输的基本示例。 进程间内存共享的优点:高性能、低延迟、资源效率高、简单可靠。 适用场景:实时图像处理、大数据传输、多进程协作、实时监控系统,等等。

qianqiansd 1997

JVM内存区域

JVM内存区域 线程公有: 在运行时数据区,方法区和堆是属于线程公有的,也就是这两块区域是循环利用的,所以要对其进行辣鸡回收。在虚拟机启动的时候创建。 线程私有: 虚拟机栈,本地方法栈,程序计数器是属于线程私有的,其与线程同生死,一次性的,所以不对其进行辣鸡回收。 方法区(线程公有) 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息,...

双目失明丝毫不影响我带崩三路 539

CSM中一些常见问题的解决方式

CSM中一些常见问题的解决方式 抖动 摄像机在每帧移动或者旋转时,会产生较严重的抖动问题。

小龙虾的博客 2131

图解JVM(二)——线程私有区

Runtime类:用于应用程序获取当前java虚拟机运行时环境的。每个JVM只有一个Runtime实例 JVM允许一个应用多线程,每个线程与操作系统的本地线程直接映射。操作系统负责线程调度到CPU,本地线程初始化成功,就会调用Java线程的run方法。 JVM的系统线程: 虚拟机线程: 周期任务线程 GC线程 编译线程:将字节码编译成本地代码 信号调度线程 程序计数器(Program Counter Register) 对物理PC寄存器的抽象模拟。存储指向下一条指令的地址。由执行引擎读取下一条指令。空间很.

u012933335的博客 2909

嵌入式开发中SAR与ΔΣ ADC选型指南:从原理到实战应用

模数转换器是嵌入式系统连接模拟世界与数字世界的核心接口,其选型直接决定了数据采集的精度与可靠性。SAR ADC采用逐次逼近原理,如同天平称重,通过二进制搜索快速完成转换,具备高速、多通道轮询的优势,适合电机控制、通信等动态场景。ΔΣ ADC则基于过采样与噪声整形技术,以速度换取精度,通过数字滤波输出高分辨率结果,在传感器测量、音频采集等静态高精度领域表现卓越。理解这两种架构的工作原理差异,是进行嵌入式ADC选型的基础。在实际工程中,需结合信号带宽、多通道切换需求、功耗约束及PCB布局能力进行综合权衡。例如,

weixin_30140399的博客 245

论文翻译:Automatic Lesson Plan Generation via Large Language Models with Self-critique Prompting

在本文中,我们利用大型语言模型(LLMs)的理解和生成能力来自动生成定制化的课程计划。这解决了传统计划可能无法充分满足不同教学环境和学生群体的特定需求的常见挑战。我们提出了一个新颖的三阶段过程,包括使用检索增强生成(RAG)逐步生成课程计划的每个关键组成部分,通过LLMs进行自我批评,以及随后的细化。我们使用这种方法为小学2至5年级生成了涵盖80多个主题的数学课程计划。邀请了三位经验丰富的教育工作者制定全面的课程计划评估标准,然后使用这些标准将我们的LLM生成的课程计划与同一主题的实际课程计划进行基准比较。

CSPhD-winston的博客 1103

JVM三:内存分区之线程私有区

Java 文件编译为 Class 文件,这时类加载器对 Class 文件进行加载,将其转化为某种静态数据结构存储到方法区,并在堆内存中生成 对象,之后便交由执行引擎来进行操作(将字节码命令转化为机器语言来执行)。编译期生成(可通过 反编译 Class 文件查看 LocalVariableTable),此时局部变量表的大小已经确定,所以在运行时也不会发生改变,主要用来存储方法的信息(参数、局部变量、基本数据类型、对象的引用地址、返回值地址)。局部变量表是一个字节数组,byte、short、char、boo

白云苍狗 1020

Agentic AI商业化落地指南:提示工程架构师的7步优化法

Agentic AI(具备自主感知、决策与行动能力的智能体系统)是未来AI商业化的核心方向,其价值在于将大模型的“认知能力”转化为“实际行动”,解决复杂商业场景中的自动化与个性化需求。而提示工程(Prompt Engineering)作为连接人类意图与智能体行为的“翻译器”,直接决定了智能体的可控性、效率与用户体验,是Agentic AI商业化落地的关键支撑。本文结合第一性原理与工程实践,提出提示工程架构师的“7步优化法”,覆盖从“商业目标定义”到“规模化推广”的全流程。通过理论框架推导架构设计拆解。

AI架构全栈开发实战笔记 1万+

线程的私有存储空间

线程的私有存储空间

weixin_52259848的博客 2847

RK3568平台(USB篇)USB基础知识

现象:把USB设备比如Android手机接到PC答1:是USB控制器,是USB控制器内嵌的root hub.答2. windows里已经有了USB的总线驱动程序,接入USB设备后,是"总线驱动程序"知道你是"android phone"提示你安装的是"设备驱动程序"。USB总线驱动程序负责:识别USB设备, 给USB设备找到对应的驱动程序。答3. PC的USB口内部,D-和D+接有15K的下拉电阻,未接USB设备时为低电平USB设备的USB口内部,D-或D+接有1.5K的上拉电阻;

weixin_49303682的博客 1658

YOLOv11 损失函数进阶:SlideLoss 让小目标检测精度飙升

创建self.loss_fun = loss_fun # 基础损失函数(如BCEWithLogitsLoss)self.loss_coef = loss_coef # 损失系数# 计算滑动权重:根据预测与真实值的差异判断样本难度# 难度越高,权重越大该类在原有损失函数的基础上,通过diff计算样本预测与真实值的差异(差异越大,样本越难),进而生成动态权重,实现对难样本的重点学习。

m0_64661770的博客 224

【JVM】JVM五大内存区域介绍

与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧 [1] (Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

qq_54247497的博客 1928

SAP 销售订单BOM使用场景简介

通常在MTO的生产场景中,我们会根据已创建的SO去展开BOM去驱动生产,如果我们生产地产品BOM是通用性的通常我们用标准BOM的方式就可以实现,但是实际在生产过程中往往不会这样,不同的客户对某些物料的规格就不相同,就会涉及产生不同的BOM,但是该客户的产品又不存在通用性,这个时候就会引入概念就是销售订单BOM,先有销售订单然后根据销售订单+行项目创建BOM。销售BOM:根据销售订单号+行项目号创建的BOM,是根据特定的销售订单或者可以理解为特定的客户创建的BOM。

我小时候很黑的博客 4226

从线程封闭到 ThreadLocal:理解并发中的私有空间

本文探讨了并发编程中的线程封闭技术及其实现方式。线程封闭通过将数据限制在单个线程中访问,避免了同步开销,是最简单的线程安全策略之一。文章介绍了三种线程封闭方式:栈封闭(利用方法局部变量)、ThreadLocal(线程局部变量)和Ad-hoc(程序控制)。重点分析了ThreadLocal的实现原理,包括Thread类中的ThreadLocalMap、ThreadLocal的哈希算法以及设置/获取值的机制。ThreadLocal通过为每个线程维护独立的数据副本,实现了高效的线程封闭,特别适合存储线程独有上下文数

weixin_48471044的博客 1166

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

上一篇: 并发容器(二)—线程安全的List
下一篇: 死锁
西红柿丶番茄
博客等级 码龄10年 21粉丝 41原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值