冰河技术
导读
♻学习路线
  • 面试必问系列

    • 面试必问
  • 架构与模式

    • Java极简设计模式
    • 实战高并发设计模式
  • Java核心技术

    • Java8新特性
    • IOC核心技术
    • JVM调优技术
  • 容器化核心技术

    • Dockek核心技术
  • 分布式存储

    • Mycat核心技术
  • 数据库核心技术

    • MySQL基础篇
  • 服务器核心技术

    • Nginx核心技术
  • 渗透核心技术

    • 渗透实战技术
  • 底层技术
  • 源码分析
  • 基础案例
  • 实战案例
  • 面试
  • 系统架构
  • Spring6核心技术
  • 分布式事务

    • 分布式事务系列视频
  • SpringBoot
  • SpringCloudAlibaba
  • 🔥AI大模型项目

    • 一站式AI智能平台
    • AI智能客服系统
    • AI智能问答系统
    • 实战AI大模型
  • 中间件项目

    • 手写高性能Redis组件
    • 手写高性能脱敏组件
    • 手写线程池项目
    • 手写高性能SQL引擎
    • 手写高性能Polaris网关
    • 手写高性能RPC项目
  • 高并发项目

    • 分布式IM即时通讯系统(新)
    • 分布式Seckill秒杀系统
    • 实战高并发设计模式
  • 微服务项目

    • 简易电商脚手架项目
  • 手撕源码

    • 手撕Spring6源码
🌍知识星球
  • 总览

    • 《书籍汇总》
  • 出版图书

    • 《深入理解高并发编程:核心原理与案例实战》
    • 《深入理解高并发编程:JDK核心技术》
    • 《深入高平行開發:深度原理&專案實戰》
    • 《深入理解分布式事务:原理与实战》
    • 《MySQL技术大全:开发、优化与运维实战》
    • 《海量数据处理与大数据技术实战》
  • 电子书籍

    • 《实战高并发设计模式》
    • 《深入理解高并发编程(第2版)》
    • 《深入理解高并发编程(第1版)》
    • 《从零开始手写RPC框架(基础篇)》
    • 《SpringCloud Alibaba实战》
    • 《冰河的渗透实战笔记》
    • 《MySQL核心知识手册》
    • 《Spring IOC核心技术》
  • 关于自己
  • 关于学习
  • 关于职场
B站
Github
导读
♻学习路线
  • 面试必问系列

    • 面试必问
  • 架构与模式

    • Java极简设计模式
    • 实战高并发设计模式
  • Java核心技术

    • Java8新特性
    • IOC核心技术
    • JVM调优技术
  • 容器化核心技术

    • Dockek核心技术
  • 分布式存储

    • Mycat核心技术
  • 数据库核心技术

    • MySQL基础篇
  • 服务器核心技术

    • Nginx核心技术
  • 渗透核心技术

    • 渗透实战技术
  • 底层技术
  • 源码分析
  • 基础案例
  • 实战案例
  • 面试
  • 系统架构
  • Spring6核心技术
  • 分布式事务

    • 分布式事务系列视频
  • SpringBoot
  • SpringCloudAlibaba
  • 🔥AI大模型项目

    • 一站式AI智能平台
    • AI智能客服系统
    • AI智能问答系统
    • 实战AI大模型
  • 中间件项目

    • 手写高性能Redis组件
    • 手写高性能脱敏组件
    • 手写线程池项目
    • 手写高性能SQL引擎
    • 手写高性能Polaris网关
    • 手写高性能RPC项目
  • 高并发项目

    • 分布式IM即时通讯系统(新)
    • 分布式Seckill秒杀系统
    • 实战高并发设计模式
  • 微服务项目

    • 简易电商脚手架项目
  • 手撕源码

    • 手撕Spring6源码
🌍知识星球
  • 总览

    • 《书籍汇总》
  • 出版图书

    • 《深入理解高并发编程:核心原理与案例实战》
    • 《深入理解高并发编程:JDK核心技术》
    • 《深入高平行開發:深度原理&專案實戰》
    • 《深入理解分布式事务:原理与实战》
    • 《MySQL技术大全:开发、优化与运维实战》
    • 《海量数据处理与大数据技术实战》
  • 电子书籍

    • 《实战高并发设计模式》
    • 《深入理解高并发编程(第2版)》
    • 《深入理解高并发编程(第1版)》
    • 《从零开始手写RPC框架(基础篇)》
    • 《SpringCloud Alibaba实战》
    • 《冰河的渗透实战笔记》
    • 《MySQL核心知识手册》
    • 《Spring IOC核心技术》
  • 关于自己
  • 关于学习
  • 关于职场
B站
Github
  • 专栏开篇

    • 开篇:用讲故事的形式带你彻底吃透并发设计模式
  • 第一篇:不可变模式

    • 第01章:这特么到底是哪里不对
    • 第02章:原来问题出在这里
    • 第03章:有哪些方法能够解决并发问题
    • 第04章:可变类的线程安全问题
    • 第05章:实现不可变类解决线程安全问题
    • 第06章:实现消息聚合发送系统
    • 第07章:JDK中的等效不可变类
  • 第二篇:保护性暂挂模式

    • 第08章:线程的流转状态
    • 第09章:解决交易过程加锁的安全性问题
    • 第10章:解决交易过程性能与死锁问题
    • 第11章:使用保护性暂挂模式优化交易系统性能
    • 第12章:基于护性暂挂模式实现监控报警系统
    • 第13章:保护性暂挂模式在JDK中的应用
  • 第三篇:两阶段终止模式

    • 第14章:线程还没执行完任务怎么就退出了
    • 第15章:到底什么是两阶段终止模式
    • 第16章:实现监控报警系统线程优雅退出
    • 第17章:两阶段终止模式在线程池中的应用
  • 第四篇:承诺模式

    • 第18章:这代码性能怎么这么差
    • 第19章:到底什么是承诺模式
    • 第20章:基于承诺模式优化社区电商项目
    • 第21章:文件同步助手项目性能太差原因分析
    • 第22章:基于承诺模式优化文件同步助手项目
    • 第23章:承诺模式在FutureTask类中的应用
  • 第五篇:生产者消费者模式

    • 第24章:面向C端的个人文库系统崩了
    • 第25章:个人文库系统资源耗尽问题分析
    • 第26章:优化面向C端的个人文库系统
    • 第27章:消息堆积问题场景分析
    • 第28章:消息堆积问题解决方案
    • 第29章:生产者消费者模式在线程池中的应用
  • 第六篇:主动对象模式

    • 第30章:重大事故访问商品链接404
    • 第31章:访问商品链接404原因分析
    • 第32章:到底什么是主动对象模式
    • 第33章:基于主动对象模式优化社区电商系统
    • 第34章:主动对象模式在线程池中的应用
  • 第七篇:线程池模式

    • 第35章:服务器内存爆了
    • 第36章:无法创建新的本地线程
    • 第37章:优化社区电商系统优惠券服务
    • 第38章:线程池核心参数解析
    • 第39章:线程池执行任务源码深度解析
    • 第40章:实现手写线程池
  • 第八篇:线程特有存储模式

    • 第41章:用户信息怎么错乱了
    • 第42章:到底什么是线程特有存储
    • 第43章:解决格式化时间的线程安全问题
    • 第44章:线程特有存储模式在JDK中的应用
    • 第45章:ThreadLocal内存泄露分析
  • 第九篇:串行线程封闭模式

    • 第46章:导出报表数据错乱了
    • 第47章:到底什么是串行线程封闭模式
    • 第48章:优化报表系统导出数据功能
  • 第十篇:主仆模式

    • 第49章:统计个数据性能太差了
    • 第50章:到底什么是主仆模式
    • 第51章:基于主仆模式优化统计热点商品功能
    • 第52章:主仆模式在JDK中的应用
  • 第十一篇:流水线模式

    • 第53章:统计个交易额也能这么慢
    • 第54章:到底什么是流水线模式
    • 第55章:基于流水线模式优化实时统计交易额功能
    • 第56章:流水线模式在Netty中的应用
  • 第十二篇:半同步半异步模式

    • 第57章:支付系统性能太差了
    • 第58章:到底什么是半同步半异步模式
    • 第59章:使用半同步半异步模式优化支付系统
    • 第60章:如何处理消息堆积问题
  • 专栏总结

    • 结尾:并发设计模式整体专栏总结

《并发设计模式》第41章-线程特有存储模式-用户信息怎么错乱了?

作者:冰河
星球:http://m6z.cn/6aeFbs
博客:https://binghe.site
文章汇总:https://binghe.site/md/all/all.html
源码获取地址:https://t.zsxq.com/0dhvFs5oR

沉淀,成长,突破,帮助他人,成就自我。

  • 本章难度:★★☆☆☆
  • 本章重点:了解线程特有存储模式的应用场景,重点理解线程特有存储模式解决线程安全的核心思路与原理,能够融会贯通,并能够结合自身项目实际场景思考如何将线程特有存储模式灵活应用到自身实际项目中。

大家好,我是冰河~~

灵魂三问:明明线上运行良好的应用服务,为何会突然出现用户信息错乱的情况呢?到底是如何产生的问题呢?我们又该如何解决这个问题呢?虽然这次生产环境出现的问题不是小菜写的代码导致的,但是小菜本着积极上进、勇于挑战苦难和解决问题的态度,还是主动站出来解决了这个问题(小菜态度真好)。

一、故事背景

这天,公司技术部门接到运营人员反馈:生产环境数据统计大盘偶尔会看到用户信息错乱的情况,这对运营查询数据统计大盘,以及对后续的运营决策产生了困扰。需要技术部门尽快排查和修复问题。技术部门接到这个任务后,作为技术部门的老大,老王经过详细的了解后,将问题交给了小菜进行处理。小菜经过排查和定位问题,这次顺利解决了问题,圆满完成了任务(真不容易,自己独立完成了,给个鸡腿作为奖励)。

二、分析问题

接到任务后,小菜立即打开研发环境排查代码逻辑,经过认真的排查、调试和定位问题,最终发现在登录接口中,代码使用了ThreadLocal来存储用户的信息,以便于在后续的业务逻辑中能够快速方便的获取用户信息。但是,最初写这个业务逻辑代码的同事可能没想到即使使用了ThreadLocal存储了用户的信息,但是用户的信息可能还是会出现错乱的情况。这是为什么呢?

从事Java开发的小伙伴都知道,一般Java服务上线后,大部分情况下是运行在Tomcat服务中,对于Tomcat服务而言,内部使用的是线程池来处理请求任务。那么重点来了,Tomcat使用了线程池,这就意味着请求可以共用Tomcat线程池中的线程,在这种情况下,如果对ThreadLcoal使用不当,就有可能出现ThreadLocal中存储的数据发生错乱的情况,如图41-1所示。


可以看到,当大量请求到来时,由于Tomcat内部使用的是线程池来处理请求任务,此时就可能会出现不同的请求共用了线程池中同一个线程的情况,线程中会使用ThreadLocal的get()方法获取数据,随后再使用ThreadLocal的set()方法存储数据,这就可能会出现数据错乱的情况。

三、重现问题

为了便于大家更好的了解问题所在,这里,我们写一段代码来模拟用户请求登录接口,登录成功后查询并存储用户信息到ThreadLocal的逻辑。源码详见:io.binghe.concurrent.design.threadlocal.wrong.ThreadLocalWrongTest。

public class ThreadLocalWrongTest {
    
    private static final ExecutorService THREAD_POOL = new ThreadPoolExecutor(1,
            1,
            1,
            TimeUnit.MINUTES,
            new LinkedBlockingQueue<>(1024));
    
    private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

    private static final int REQUEST_COUNT = 2;

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(REQUEST_COUNT);
        System.out.println("重现问题开始");
        long startTime = System.currentTimeMillis();
        for (int i = 1; i <= REQUEST_COUNT; i++){
            int count = i;
            THREAD_POOL.execute(() -> {
                //先从THREAD_LOCAL中获取数据
                String username = THREAD_LOCAL.get();
                System.out.println("第" + count + "个请求第1次获取到的数据为:" + username);
                //存储数据到THREAD_LOCAL中
                THREAD_LOCAL.set("binghe00" + count);
                //再次从THREAD_LOCAL中查询数据
                username = THREAD_LOCAL.get();
                System.out.println("第" + count + "个请求第2次获取到的数据为:" + username);
                countDownLatch.countDown();
            });
        }
        System.out.println("重现问题结束,耗时:" + (System.currentTimeMillis() - startTime) + "ms");
        countDownLatch.await();
        THREAD_POOL.shutdown();
    }
}

可以看到,在ThreadLocalWrongTest类的代码中,创建了一个核心线程数和最大线程数都为1的线程池,来模拟线程池中只有一个线程的Tomcat服务。同时,定义了一个THREAD_LOCAL常量来模拟获取和存储用户信息,并且以请求两次为例重现问题。接下来,我们看看main()方法的实现。

main()方法中,最主要的逻辑就是在for循环,这里for循环模拟的是请求次数,由于线程池核心线程数和最大线程数都是1,此时有两个请求到来,那两个请求就一定会共用线程池中唯一的一个线程。此时,我们在for循环中将模拟的请求提交到线程池。首先,会从THREAD_LOCAL中获取数据并进行打印,随后会向THREAD_LOCAL中存储数据,接下来,再次从THREAD_LOCAL中获取数据并进行打印。按理说,两个请求之前没有影响才对,可事实却是在第2次请求中会获取到第1次请求存储到THREAD_LOCAL中的数据。

查看全文

加入冰河技术知识星球,解锁完整技术文章与完整代码

在 GitHub 上编辑此页
上次更新: 2026/4/29 16:18
Contributors: binghe001
Next
第42章:到底什么是线程特有存储
阅读全文
×

扫码或搜索:冰河技术
发送:290992
即可立即永久解锁本站全部文章

星球会员
跳转链接