禅与计算机 禅与计算机
首页
  • Java基础

    • 聊一聊java一些核心知识点
    • 聊聊java面向对象核心知识点
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于提示词工程的Redis签到功能开发实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 写作

    • 写好技术博客的5大核心原则:从认知科学到AI工具的全流程指南
  • 开发工具

    • IDEA配置详解与高效使用指南
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • 从Lucene到Elasticsearch:进化之路
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • 一文带你速通Go语言基础语法
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

sharkchili

计算机禅修者
首页
  • Java基础

    • 聊一聊java一些核心知识点
    • 聊聊java面向对象核心知识点
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于提示词工程的Redis签到功能开发实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 写作

    • 写好技术博客的5大核心原则:从认知科学到AI工具的全流程指南
  • 开发工具

    • IDEA配置详解与高效使用指南
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • 从Lucene到Elasticsearch:进化之路
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • 一文带你速通Go语言基础语法
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java基础

  • 并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • 浅谈传统并发编程的优化思路
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • 浅谈池化技术的优雅关闭
    • 浅谈守护线程与进程优雅关闭
    • 浅谈并发编程等待通知模型
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 深入理解synchronized同步机制
    • 详解JUC包下的锁
    • 详解JUC包下各种锁的使用
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • 详解Java并发流程控制工具
    • Java并发容器总结
    • 深入解析CopyOnWriteArrayList
    • 详解Java并发编程volatile关键字
    • 聊聊JVM中安全点的概念
    • 并发编程ThreadLocal必知必会
      • 写在文章开头
      • 详解ThreadLocal
        • 什么是ThreadLocal?它有什么用?
        • ThreadLocal基础使用示例
      • ThreadLocal两种应用场景
        • 日期格式化
        • 服务间调用的线程变量共享
      • ThreadLocal使用注意事项
        • 内存泄漏问题
        • 空指针问题
        • 线程重用问题
      • 基于源码了解ThreadlLocal工作原理
        • ThreadlLocal如何做到线程隔离的?
        • ThreadLocalMap有什么特点?和HashMap有什么区别
        • ThreadLocal探测式清理和启发式清理
      • ThreadLocal的不可继承性
        • 通过代码证明ThreadLocal的不可继承性
        • 使用InheritableThreadLocal实现主线程内部变量继承
        • 基于源码剖析原因
      • ThreadLocal在Spring中的运用
      • 为什么JDK建议将ThreadLocal设置为static
      • 小结
      • 参考
    • CompletableFuture基础实践小结
    • CompletableFuture异步IO密集型任务最佳实践
    • CompletableFuture异步多任务最佳实践
    • CompletableFuture组合流水线任务实践
    • 实现一个简单实用的的并发同步模型
    • 解决Java并发问题的常见思路
    • 来聊一个有趣的限流器RateLimiter
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 记一个ConcurrentHashMap使用不当导致的并发事故
    • JS设计模式总结笔记
    • 小程序笔记
    • 《JavaScript教程》笔记
  • JVM相关

  • 深入理解Spring框架

  • Java核心技术
  • 并发编程
sharkchili
2026-03-25
目录

并发编程ThreadLocal必知必会

[toc]

# 写在文章开头

在多线程编程中,共享资源的管理和同步一直是开发人员面临的挑战之一。ThreadLocal 是 Java 提供的一种简单而强大的机制,用于实现线程局部变量,即每个线程都有自己的独立副本,互不干扰。这种机制不仅简化了并发编程中的数据管理,还提高了代码的可读性和可维护性。

我是 SharkChili,Java 开发者,Java Guide开源项目维护者。欢迎关注我的公众号:写代码的SharkChili,也欢迎您了解我的开源项目 mini-redis:https://github.com/shark-ctrl/mini-redis。

为方便与读者交流,现已创建读者群。关注上方公众号获取我的联系方式,添加时备注加群即可加入。

# 详解ThreadLocal

# 什么是ThreadLocal?它有什么用?

为了保证特定变量对当前线程可见,我们就可以使用ThreadLocal关键字,ThreadLocal可以为每个线程创建这个变量的副本并存到每个线程的存储空间中(关于这个存储空间后文会展开讲述),从而确保共享变量对每个线程隔离:

# ThreadLocal基础使用示例

如上文所说ThreadLocal最典型的用法就是维护各个线程各自需要独享变量,基于ThreadLocal为每个将每个线程的id存到线程内部,彼此之间互不影响。

 ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

        Thread t1 = new Thread(() -> {
            THREAD_LOCAL.set("thread-0");
            Console.log("thread-0获取线程内部缓存值:{}", THREAD_LOCAL.get());
        }, "t0");


        Thread t2 = new Thread(() -> {
            THREAD_LOCAL.set("thread-1");
            Console.log("thread-1获取线程内部缓存值:{}", THREAD_LOCAL.get());
        }, "t1");

        t1.start();
        t2.start();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

从输出结果可以看出,两个线程都用THREAD_LOCAL 在自己的内存空间中存储了变量的副本,彼此互相隔离的使用

thread-1获取线程内部缓存值:thread-1
thread-0获取线程内部缓存值:thread-0
1
2

# ThreadLocal两种应用场景

# 日期格式化

我们经常会使用SimpleDateFormat执行日期格式化输出,为了避免频繁的创建SimpleDateFormat的繁琐和开销,我们可能会编写出下面这样一段代码:

private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SS");

    /**
     * 将传入的秒转为时间字符串
     * @param second
     * @return
     */
    public static String convert2DateStr(int second) {
        Date date = new Date(second);
        return dateFormat.format(date);
    }
1
2
3
4
5
6
7
8
9
10
11

对应我们也给出相应的使用示例:

//跑两次传入1970-01-01 08:00:00:00和1970-01-01 08:00:01:00的数据
        for (int i = 0; i < 2; i++) {
            int finalI = i;
            new Thread(() -> {
                String str = convert2DateStr(1000 * finalI);
                Console.log("转换结果:{}", str);
            }).start();
        }
1
2
3
4
5
6
7
8

输出结果如下,可以发现不同的时间却输出相同的结果:

原因也很简单,SimpleDateFormat进行format时会通过calendar存储当前转换的日期,并发情况下,很可能其他线程会将当前calendar的值覆盖,这也就是为什么我们线程0输出了和线程1一样的结果:

对应我们也给出SimpleDateFormat的format函数底层实现,可以看到其底层在转换前会通过calendar存储当前需要转换的日期的值:

@Override
    public StringBuffer format(Date date, StringBuffer toAppendTo,
                               FieldPosition pos)
    {
        //......
      //调用format完成日期格式化字符串转换
        return format(date, toAppendTo, pos.getFieldDelegate());
    }

    // Called from Format after creating a FieldDelegate
    private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
        // 将calendar设置为需要格式化的日期
        calendar.setTime(date);
      
     //...... 
    }   
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

基于该问题我们使用ThreadLocal为线程分配SimpleDateFormat,本质上就是针对每个线程分配一个专门的SimpleDateFormat:

private static ThreadLocal<SimpleDateFormat> THREAD_LOCAL = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SS"));

    /**
     * 将传入的秒转为时间字符串
     *
     * @param second
     * @return
     */
    public static String convert2DateStr(int second) {
        Date date = new Date(second);
        return THREAD_LOCAL.get().format(date);
    }
1
2
3
4
5
6
7
8
9
10
11
12

# 服务间调用的线程变量共享

我们日常web开发都会涉及到各种service的调用,例如某个controller需要调用完service1之后再调用service2。因为我们的controller和service都是单例的,所以如果我们希望多线程调用这些controller和service保证共享变量的隔离,也可以用到ThreadLocal。

为了实现这个示例,我们编写了线程获取共享变量的工具类:





public class MyUserContextHolder {
    private static ThreadLocal<User> holder = new ThreadLocal<>();

    public static ThreadLocal<User> getHolder() {
        return holder;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12

service调用链示例如下,笔者创建service1之后,所有线程复用这个service完成了调用,并且在服务间调用直接通过ThreadLocal完成了线程副本共享:

public class MyThreadLocalGetUserId {

    private static Logger logger = LoggerFactory.getLogger(MyThreadLocalGetUserId.class);

    private static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            MyService1 service1 = new MyService1();
            threadPool.submit(() -> {

                service1.doWork1("username" + (finalI+1));
            });

        }


    }
}


class MyService1 {

    private static Logger logger = LoggerFactory.getLogger(MyThreadLocalGetUserId.class);

    public void doWork1(String name) {

        logger.info("service1 存储userName:" + name);
        ThreadLocal<String> holder = MyUserContextHolder.getHolder();
        holder.set(name);
        MyService2 service2 = new MyService2();
        service2.doWork2();
    }

}

class MyService2 {
    private static Logger logger = LoggerFactory.getLogger(MyThreadLocalGetUserId.class);

    public void doWork2() {
        ThreadLocal<String> holder = MyUserContextHolder.getHolder();

        logger.info("service2 获取userName:" + holder.get());
        MyService3 service3 = new MyService3();
        service3.doWork3();
    }
}


class MyService3 {
    private static Logger logger = LoggerFactory.getLogger(MyThreadLocalGetUserId.class);

    public void doWork3() {
        ThreadLocal<String> holder = MyUserContextHolder.getHolder();

        logger.info("service3获取 userName:" + holder.get());

// 避免oom问题
        holder.remove();
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

从输出结果来看,在单例对象情况下,既保证了同一个线程间变量共享。

也保证了不同线程之间变量的隔离。

# ThreadLocal使用注意事项

# 内存泄漏问题

我们有下面这样一段web代码,每次请求test0就会像线程池中的线程存一个4M的byte数组:

RestController
public class TestController {
    final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(100, 100, 1, TimeUnit.MINUTES,
            new LinkedBlockingQueue<>());// 创建线程池,通过线程池,保证创建的线程存活

    final static ThreadLocal<Byte[]> localVariable = new ThreadLocal<Byte[]>();// 声明本地变量

    @RequestMapping(value = "/test0")
    public String test0(HttpServletRequest request) {
        poolExecutor.execute(() -> {
            Byte[] c = new Byte[4* 1024* 1024];
            localVariable.set(c);// 为线程添加变量

        });
        return "success";
    }

   
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

我们将这个代码打成jar包部署到服务器上并启动

java -jar -Xms100m -Xmx100m # 调整堆内存大小
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof  # 表示发生OOM时输出日志文件,指定path为/tmp/heapdump.hprof
-XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/tmp/heapTest.log # 打印日志、gc时间以及指定gc日志的路径
demo-0.0.1-SNAPSHOT.jar

1
2
3
4
5

只需频繁调用几次,就会输出OutOfMemoryError

Exception in thread "pool-1-thread-5" java.lang.OutOfMemoryError: Java heap space
        at com.example.jstackTest.TestController.lambda$test0$0(TestController.java:25)
        at com.example.jstackTest.TestController$$Lambda$582/394910033.run(Unknown Source)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)
1
2
3
4
5
6

问题的根本原因是我们没有及时回收Thread从ThreadLocal中得到的变量副本。因为我们的使用的线程是来自线程池中,所以线程使用结束后并不会被销毁,这就使得ThreadLocal中的变量副本会一直存储与线程池中的线程中,导致OOM。

可能你会问了,不是说Java有GC回收机制嘛?为什么还会出现Thread中的ThreadLocalMap的value不会被回收呢?

我们上文提到ThreadLocal得到值,都会以ThreadLocal为key,ThreadLocal的initialValue方法得到的value作为值生成一个entry对象,存到当前线程的ThreadLocalMap中。 而我们的Entry的key是一个弱引用,一旦我们使用的threadLocal临时变量用完被垃圾回收之后,这个key就会因为弱引用(只要垃圾回收器启动就会被回收)的原因被回收,而我们这个key所对应的value仍然被线程池中的线程的强引用引用着,所以就迟迟无法回收,随着时间推移每个线程都出现这种情况导致OOM。

所以我们每个线程使用完ThreadLocal之后,一定要使用remove方法清楚ThreadLocalMap中的value:

localVariable.remove()
1

从源码中可以看到remove方法会遍历当前线程map然后将强引用之间的联系切断,确保下次GC可以回收掉可以无用对象。

private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            //定位,并将entry清除
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 空指针问题

使用ThreadLocal存放包装类的时候也需要注意添加初始化方法,否则在拆箱时可能会出现空指针问题。

 private  static ThreadLocal<Long> threadLocal = new ThreadLocal<>();


    public static void main(String[] args) {
        Long num = threadLocal.get();
        long sum=1+num;

    }

1
2
3
4
5
6
7
8
9

输出错误:

Exception in thread "main" java.lang.NullPointerException
	at com.guide.base.MyThreadLocalNpe.main(MyThreadLocalNpe.java:11)
1
2

解决方式

 private  static ThreadLocal<Long> threadLocal = ThreadLocal.withInitial(()->new Long(0));
1

# 线程重用问题

这个问题和OOM问题类似,在线程池中服用同一个线程未及时清理,导致下一次HTTP请求时得到上一次ThreadLocal存储的结果。


ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> null);


 * 线程池中使用threadLocal示例
     *
     * @param accountCode
     * @return
     */
    @GetMapping("/account/getAccountByCode/{accountCode}")
    @SentinelResource(value = "getAccountByCode")
    ResultData<Map<String, Object>> getAccountByCode(@PathVariable(value = "accountCode") String accountCode) throws InterruptedException {
        Map<String, Object> result = new HashMap<>();
        
        CountDownLatch countDownLatch = new CountDownLatch(1);
        
        
        threadPool.submit(() -> {

            String before = Thread.currentThread().getName() + ":" + threadLocal.get();
            log.info("before:" + before);
            result.put("before", before);

            log.info("调用getByCode,请求参数:{}", accountCode);
            QueryWrapper<Account> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("account_code", accountCode);
            Account account = accountService.getOne(queryWrapper);

            String after = Thread.currentThread().getName() + ":" + account.getAccountName();
            result.put("after", account.getAccountName());
            log.info("after:" + after);

            threadLocal.set(account.getAccountName());
            
            //完成计算后,使用countDown按下倒计时门闩,通知主线程可以执行后续步骤
            countDownLatch.countDown();

        });

        //等待上述线程池完成
        countDownLatch.await();

        return ResultData.success(result);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

从输出结果可以看出,我们第二次进行HTTP请求时,threadLocal第一get获得了上一次请求的值,出现脏数据。


C:\Users\xxx>curl http://localhost:9000/account/getAccountByCode/demoData
{"status":100,"message":"操作成功","data":{"before":"pool-2-thread-1:null","after":"pool-2-thread-1:demoData"},"success":true,"timestamp":1678410699943}
C:\Users\xxx>curl http://localhost:9000/account/getAccountByCode/Zsy
{"status":100,"message":"操作成功","data":{"before":"pool-2-thread-1:demoData","after":"pool-2-thread-1:zsy"},"success":true,"timestamp":1678410707473}
1
2
3
4
5

解决方法也很简单,手动添加一个threadLocal的remove方法即可

@GetMapping("/account/getAccountByCode/{accountCode}")
    @SentinelResource(value = "getAccountByCode")
    ResultData<Map<String, Object>> getAccountByCode(@PathVariable(value = "accountCode") String accountCode) throws InterruptedException {
        Map<String, Object> result = new HashMap<>();

        CountDownLatch countDownLatch = new CountDownLatch(1);

        try {
            threadPool.submit(() -> {

                String before = Thread.currentThread().getName() + ":" + threadLocal.get();
                log.info("before:" + before);
                result.put("before", before);

                log.info("调用getByCode,请求参数:{}", accountCode);
                QueryWrapper<Account> queryWrapper = new QueryWrapper<>();
                queryWrapper.eq("account_code", accountCode);
                Account account = accountService.getOne(queryWrapper);

                String after = Thread.currentThread().getName() + ":" + account.getAccountName();
                result.put("after", after);
                log.info("after:" + after);

                threadLocal.set(account.getAccountName());

                //完成计算后,使用countDown按下倒计时门闩,通知主线程可以执行后续步骤
                countDownLatch.countDown();

            });
        } finally {
            threadLocal.remove();
        }


        //等待上述线程池完成
        countDownLatch.await();

        return ResultData.success(result);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# 基于源码了解ThreadlLocal工作原理

# ThreadlLocal如何做到线程隔离的?

我们下面这段代码为例进行分析,本质上ThreadLocal的withInitial指明了每个线程初始化时设置默认值:

ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SS"));
1

当我们执行get操作时,threadLocal 就会为当前线程完成内部map的初始化,然后通过initialValue获取上一步声明的SimpleDateFormat实例,由此保证每个线程内部都有一个独有的SimpleDateFormat:

img

对应的我们给出ThreadlLocal的get的源码,整体逻辑与上述差不多,即初始化线程内部的map,然后通过setInitialValue调用initialValue创建初始值存到线程的map中:

public T get() {
    //获取当前线程
        Thread t = Thread.currentThread();
        //拿到当前线程中的map
        ThreadLocalMap map = getMap(t);
        //如果map不为空则取用当前这个ThreadLocal作为key取出值,否则通过setInitialValue完成ThreadLocal初始化
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }


private T setInitialValue() {
    //执行initialValue为当前线程创建变量value,在这里也就是我们要用的SimpleDateFormat 
        T value = initialValue();
        //获取当前线程map,有则直接以ThreadLocal为key将SimpleDateFormat 设置进去,若没有先创建再设置
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
//返回SimpleDateFormat 
        return value;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# ThreadLocalMap有什么特点?和HashMap有什么区别

我们通过源码查看到这个map为ThreadLocalMap,它是由一个个Entry 构成的数组:

 private Entry[] table;
1

并且每个Entry 的key是弱引用,这就意味着当触发GC时,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;
            }
        }

     
       
1
2
3
4
5
6
7
8
9
10
11
12

除上面所说,thread中的map和hashmap还有一个不同点就是数据结构,因为threadLocal的适用场景特殊,所以大部分情况下其内部存储空间不会存储太多元素,所以出于简单的考虑,线程中的map本质上就是一个数组,一旦发生冲突则直接通过线性探测法找到数组中空闲的位置将值存入:

 private void set(ThreadLocal<?> key, Object value) {

           //......

            Entry[] tab = table;
            int len = tab.length;
            //定位键值对存储的索引位置
            int i = key.threadLocalHashCode & (len-1);
      //通过线性探测法循环找到空闲位置存入元素
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

               //......
            }
      //找到合适的位置将元素存入
            tab[i] = new Entry(key, value);
            //更新一下容量信息
            int sz = ++size;
            //......
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# ThreadLocal探测式清理和启发式清理

上文中我们介绍了ThreadLocal使用不当所导致的内存泄漏问题,实际上Josh Bloch and Doug Lea也考虑过该问题,并在实现中也有对这些问题做一定的考虑,即探测式清理和启发式清理。

我们先来说说探测式清理,它一般会在如下几个时机触发:

  1. 调用get方法未找到元素时,调用getEntryAfterMiss触发探测式清理
  2. 调用remove方法时,完成对应的元素清理后
  3. 底层map容量达到阈值时,触发rehash
private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
  					//遍历threadLocal 底层的map
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
              //如果ket为空,就说明该key对应value没有被使用,可直接设置为null等待gc
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                   //......
                }
            }
            return i;
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

另一种则是探测式清理,它一般时跟随着threadLocal写入新元素或者覆盖新元素时触发的set和replaceStaleEntry,它会从操作的索引i开始遍历并清理value:

private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
              	//调用e的get方法为空,说明这个key即对应的线程的threadLocal被回收
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

需要强调的是,这两种清理方式都有一定的局限性,使得thread map中key为空只会在触发如下3个条件:

  1. threadLocal被垃圾回收
  2. key作为弱引用被回收
  3. 触发探测式或者启动式清理

这也就意味着如果我们的threadLocal采用static修饰后,对应的元空间threadLocal引用就会强引用着堆区的threadlocal实例,也就说theadlocal就会跟随类对象存在于GC堆中,即与类对象生命周期相同,除非threadLocal对应类被卸载,否则这个threadLocal变量就不可能被GC即每个线程内部的value都无法被探测式或者启发式清理掉,如果没有显示remove每个线程中threadLocal的map的话,还是存在内存溢出的风险:

thread-local-6.drawio

# ThreadLocal的不可继承性

# 通过代码证明ThreadLocal的不可继承性

如下代码所示,ThreadLocal子线程无法拿到主线程维护的内部变量

/**
 * ThreadLocal 不具备可继承性
 */
public class ThreadLocalInheritTest {
    private static ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

    private static Logger logger = LoggerFactory.getLogger(ThreadLocalInheritTest.class);

    public static void main(String[] args) {
        THREAD_LOCAL.set("mainVal");
        logger.info("主线程的值为: " + THREAD_LOCAL.get());

        new Thread(() -> {
            try {
                //睡眠3s确保上述逻辑运行
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            logger.info("子线程获取THREAD_LOCAL的值为:[{}]", THREAD_LOCAL.get());
        }).start();
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 使用InheritableThreadLocal实现主线程内部变量继承

如下所示,我们将THREAD_LOCAL 改为InheritableThreadLocal类即可解决问题。

/**
 * ThreadLocal 不具备可继承性
 */
public class ThreadLocalInheritTest {

    private static ThreadLocal<String> THREAD_LOCAL = new InheritableThreadLocal<>();

    private static Logger logger = LoggerFactory.getLogger(ThreadLocalInheritTest.class);

    public static void main(String[] args) {
        THREAD_LOCAL.set("mainVal");
        logger.info("主线程的值为: " + THREAD_LOCAL.get());

        new Thread(() -> {
            try {
                //睡眠3s确保上述逻辑运行
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            logger.info("子线程获取THREAD_LOCAL的值为:[{}]", THREAD_LOCAL.get());
        }).start();
    }

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 基于源码剖析原因

因为 ThreadLocal会将变量存储在线程的 ThreadLocalMap中,所以我们先看看InheritableThreadLocal的getMap方法,从而定位到了inheritableThreadLocals:

 ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
1
2
3

然后我们到Thread类去定位这个变量的使用之处,所以我们在创建线程的地方打了个断点:

从而定位到这段初始化,它会获取主线程的ThreadLocalMap并将主线程ThreadLocalMap中的值存到子线程的ThreadLocalMap中。

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

		//获取当前线程的主线程
        Thread parent = currentThread();
       
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
            //将主线程的map的值存到子线程中
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
       	//......
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

createInheritedMap内部就会调用ThreadLocalMap方法将主线程的ThreadLocalMap的值存到子线程的ThreadLocalMap中。

private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];
			//遍历父线程数据复制到子线程map中
            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                    	//......
                    	//定位当前子线程bucket位置将value存入
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# ThreadLocal在Spring中的运用

其实针对日期格式化问题,Spring已经为我们内置好了相应的工具类即DateTimeContextHolder:

	private static final ThreadLocal<DateTimeContext> dateTimeContextHolder =
			new NamedThreadLocal<>("DateTimeContext");
1
2

该工具类和simpledateformate差不多,使用示例如下所示,是spring封装的,使用起来也很方便:

public class DateTimeContextHolderTest {


    protected static final Logger logger = LoggerFactory.getLogger(DateTimeContextHolderTest.class);

    private final static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");

    private Set<String> set = new ConcurrentHashSet<String>();

    @Test
    public void test_withLocale_same() throws Exception {
        ExecutorService threadPool = Executors.newFixedThreadPool(30);

        for (int i = 0; i < 30; i++) {
            int finalI = i;
            threadPool.execute(() -> {
                LocalDate currentdate = LocalDate.now();
                int year = currentdate.getYear();
                int month = currentdate.getMonthValue();
                int day = 1 + finalI;
                LocalDate date = LocalDate.of(year, month, day);

                DateTimeFormatter fmt = DateTimeContextHolder.getFormatter(formatter, null);
                String text = date.format(fmt);
                set.add(text);
                logger.info("转换后的时间为" + text);
            });
        }

        threadPool.shutdown();
        while (!threadPool.isTerminated()) {

        }

        logger.info("查看去重后的数量"+set.size());


    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# 为什么JDK建议将ThreadLocal设置为static

我们都知道使用static是属于类,存在于方法区中,即修饰的变量是全局共享的,这意味着当前ThreadLocal在通过static之后,即所有的实例对象都共享一个ThreadLocal。从而避免重复创建TSO(Thread Specific Object)即ThreadLocal所关联的对象的创建的开销, 以及这种方案使得即使出现内存泄漏也是O(1)级别的内存泄露。对应的实例变量的ThreadLocal的O(n)内存泄漏,这就不必多说。

# 小结

  1. ThreadLocal通过在将共享变量拷贝一份到每个线程内部的ThreadLocalMap保证线程安全。
  2. ThreadLocal使用完成后记得使用remove方法手动清理线程中的ThreadLocalMap过期对象,避免OOM和一些业务上的错误。
  3. ThreadLocal是不可被继承了,如果想使用主线的的ThreadLocal,就必须使用InheritableThreadLocal。

我是 SharkChili,Java 开发者,Java Guide开源项目维护者。欢迎关注我的公众号:写代码的SharkChili,也欢迎您了解我的开源项目 mini-redis:https://github.com/shark-ctrl/mini-redis。

为方便与读者交流,现已创建读者群。关注上方公众号获取我的联系方式,添加时备注加群即可加入。

# 参考

Java 并发 - ThreadLocal详解:https://www.pdai.tech/md/java/thread/java-thread-x-threadlocal.html#java-开发手册中推荐的-threadlocal (opens new window)

面试:为了进阿里,死磕了ThreadLocal内存泄露原因:https://www.cnblogs.com/Ccwwlx/p/13581004.html (opens new window)

ThreadLocal出现OOM内存溢出的场景和原理分析:https://www.cnblogs.com/jobbible/p/13364292.html#:~:text=取消注释:threadLocal.remove ();,结果不会出现OOM,可以看出堆内存的变化呈现锯齿状,证明每一次remove ()之后,ThreadLocal的内存释放掉了! (opens new window)

将ThreadLocal变量设置为private static的好处是啥? - Viscent大千的回答 - 知乎 :https://www.zhihu.com/question/35250439/answer/101676937 (opens new window)

ThreadLocalMap.key到期之'探测是清理'+'启发式清理'流程:https://www.cnblogs.com/lihw/p/17215370.html (opens new window)

Where Java Stores Static vs. Instance Data in Memory:https://medium.com/@AlexanderObregon/where-java-stores-static-vs-instance-data-in-memory-bcfb00ee73cf (opens new window)

编辑 (opens new window)
上次更新: 2026/03/26, 01:05:31
聊聊JVM中安全点的概念
CompletableFuture基础实践小结

← 聊聊JVM中安全点的概念 CompletableFuture基础实践小结→

最近更新
01
基于EasyExcel实现高效导出
03-25
02
从开源框架中学习那些实用的位运算技巧
03-25
03
浅谈分布式架构设计思想和常见优化手段
03-25
更多文章>
Theme by Vdoing | Copyright © 2025-2026 Evan Xu | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×
×