disgare 的博客
首页
博客
分类
标签
首页
博客
分类
标签
  • 网络

    • 计算机网络学习笔记
    • 网络安全相关
    • 域名和子网掩码
    • CORS 跨域资源共享
    • DNS、HTTP 与 HTTPS
    • Server-Sent Events (SSE)
    • WebSocket 长连接
  • 计算机基础

    • 操作系统 IO 相关知识
    • 操作系统学习笔记
    • 程序的机器级表示
    • 音频文件基础
    • 正则表达式相关概念
    • ffmpeg 的安装以及实现音频切分功能
    • Hex 和 Base64 编码
    • XML 的使用
  • 数据结构与算法

    • 动态规划算法学习笔记
    • 基于比较的排序算法的最坏情况下的最优下界为什么是O(nlogn)
    • 集合与数据结构学习笔记
    • 面试常见算法总结
    • 算法导论第二部分排序学习笔记
    • 算法导论第一部分学习笔记
  • Java

    • 对象之间的映射与转换
    • 反射学习笔记
    • 泛型相关概念
    • 关于 boolean 类型的坑
    • 如何使用 lambda 表达式实现排序
    • CompletableFuture 相关用法
    • CompletableFuture 源码浅要阅读
    • FutureTask 源码阅读
    • Guava 常用 API
    • Guava 源码阅读:Multimap 相关
    • Jackson 的各种使用
    • Java 的 Excel 相关操作
    • java 的常见性能问题分析以及出现场景
    • java 基础知识
    • JAVA 枚举的基础和原理
    • Java 图片文件上传下载处理
    • Java 序列化
    • Java 异常
    • Java 语法糖
    • Java 中关于字符串处理的常用方法
    • Java 中强、软、弱、虚引用
    • JAVA 注解小结
    • Java Http 访问框架
    • Java Stream 的使用
    • Java8 新特性
    • netty 学习笔记
    • Scanner 的各种用法
    • Servlet 学习笔记
    • String、StringBuffer、StringBuilder 学习笔记
  • JVM

    • 虚拟机执行子系统
    • JVM 自动内存管理
    • Linux 中 JVM 常用工具以及常见问题解决思路
  • Linux

    • crontab 表达式
    • Linux 常见命令
    • Linux 文件系统
  • 中间件

    • 关于定时任务原理
    • 详解 kafka
    • ES 搜索引擎
    • flink 提交流程
    • Grape-RAG
    • Hadoop 基础原理
  • 多线程

    • 多线程基础学习笔记
    • 简单了解并发集合
    • 如何手写单例
    • 深入理解 java 多线程安全
    • 生产者消费者问题
    • 线程池作用、用法以及原理
    • AQS 组件
    • ThreadLocal 原理以及使用
      • 用法
      • 底层结构
      • 为啥要使用弱引用
      • 碰撞处理
  • 非关系型数据库

    • Redis 集群
    • Redis 数据结构、对象与数据库
    • Redis 学习笔记
  • 关系型数据库

    • B+ 树的插入、删除和数据页分裂机制
    • MySQL 的 binglog、redolog、undolog
    • MySQL 的记录存储结构、存储引擎与 Buffer Pool
    • MySQL 基本的特性
    • MySQL 开发规范
    • MySQL 事务与锁与 MVCC
    • MySQL 数据类型、字符集相关内容
    • MySQL 索引与索引优化
    • PostgreSQL 更新数据时 HOT优化
    • PostgreSQL 相关用法
  • Python

    • Python 基础语法
    • Python 学习
  • Spring 项目

    • Lombok 的常用注解
    • maven 小结
    • MyBatis 框架的使用
    • MyBatis 重要知识点总结
    • MybatisPlus 的使用
    • Spring 框架基础使用
    • Spring 事务相关
    • Spring IOC 的原理及源码
    • Spring AOP 的使用和原理
    • SpringBoot 的原理
    • SpringBoot 基础使用
    • SpringWeb 重要知识点
  • 分布式

    • 初步了解 docker
    • 从 ACID 到 BASE 事务处理的实现
    • 访问远程服务
    • 分布式 id
    • 分布式缓存相关问题
    • 分布式集群理论和分布式事务协议
    • 分布式架构的观测
    • 分布式一致性算法
    • 负载均衡 Load Balancing
    • 关于分布式系统 RPC 中高可用功能的实现
    • 集群间数据同步的目的
    • 三高问题下的系统优化
    • 数据库分库分表
    • 详解 Spring Cloud
    • Dubbo 基础概念
    • Gossip 协议
    • nginx 学习笔记
    • Protobuf 通信协议
    • Zookeeper 基础学习
  • 架构设计

    • 参数校验与异常处理
    • 抽象方法与设计模式
    • 代码整洁之道
    • 权限系统设计
    • 用低内存处理大量数据
    • 设计模式——策略模式
    • 设计模式——过滤器模式在 Spring 中的实践
    • 状态模式
    • 统一结果返回
    • 为什么要打日志?怎么打日志?打什么日志?
    • 运维监控常见指标含义
    • 资深研发进阶
    • DDD 架构学习笔记
    • Java 常用的规则引擎
    • MVC 架构学习笔记
  • AI

    • 如何编写 Prompt
    • Agent 工程架构
    • LLM 相关内容
    • NLP 相关知识
    • vibe coding 最佳实践
    • windows 下 ollama 迁移到 D 盘
  • 开发工具

    • 如何画时序图、流程图、状态流转图
    • excel 关于 =vlookup 的用法
    • git 的学习以及使用
    • IDEA 插件推荐
    • IDEA 常用快捷键以及调试
    • Shell 脚本
    • swagger 的使用
  • 前端

    • 简单了解前端页面开发
    • 伪静态是什么
    • GitHub Pages 部署教程
    • Vercel 部署教程
    • vue-admin-template 简单使用
    • VuePress 博客搭建指南
  • 项目

    • 面试刷题网——技术方案
    • 影视资源聚合站——技术方案
  • 问题记录

    • 定时任务单线程消费 redis 中数据导致消费能力不足
    • 提供可传递的易受攻击的依赖项
    • Liteflow 在 SpringBoot 启动时无法注入组件问题 couldn‘t find chain with the id[THEN(NodeComponent)]
  • 金融

    • 股票分析——关于电力
    • 股票技术面——量价关系
    • 股票技术面——盘口
    • 股票技术面——基础
    • 基础的金融知识
    • 基金与股票
    • 韭菜的自我总结
    • 聊聊价值投资
  • 其他

    • 程序员职场工作需要注意什么
    • 创业全链路SOP:从灵光一现到系统化增长的实战指南
    • 观罗翔讲刑法随笔
    • 价格和价值
    • 立直麻将牌效益理论
    • 梅花易数学习笔记
    • 压力管理
2023-01-03
多线程
目录

ThreadLocal 原理以及使用

在多线程(java web)情况下,在线程类中使用 ThreadLocal 可以为每个线程配置私有的对象

    ThreadLocal<Object> threadLocal = new ThreadLocal<>();
1

# 用法

比如可以这样使用,用 ThreadLocal 保存用户信息。在用户登录拦截时,通过校验的用户可以将该用户常用信息放进 ThreadLocal,在这次请求时可以随时取出来使用,不是公用属性不会存在多线程并发问题:

public class RequestContextCache {

    private static final ThreadLocal<CrmUserInfo> USER_INFO_CACHE = new ThreadLocal<>();

    public static HttpServletRequest getRequest() {
        return (RequestContextHolder.getRequestAttributes()) == null ?
                null : ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    }

    public static HttpServletResponse getResponse() {
        return ((ServletWebRequest) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getResponse();
    }

    public static CrmUserInfo getUser() {
        return USER_INFO_CACHE.get();
    }

    public static void updateStatus(CrmUserStatusEnum crmUserStatusEnum) {
        USER_INFO_CACHE.get().setStatus(crmUserStatusEnum);
    }

    public static void register(CrmUserInfo loginUserInfo) {
        if (USER_INFO_CACHE.get() != null) {
            return;
        }
        USER_INFO_CACHE.set(loginUserInfo);
    }

    public static void clear() {
        USER_INFO_CACHE.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

# 底层结构

这个类中只有一个 ThreadLocal 对象,但是每一个线程中都有不同的 ThreadLocalMap,Thread 中定义了 ThreadLocal.ThreadLocalMap threadLocals = null;

最终的变量是放在了当前线程的 ThreadLocalMap 中,并将 ThreadLocal 这个对象的弱引用作为键。而 ThreadLocalMap 被定义成了 Thread 类的成员变量

	// ThreadLocal 的 set 方法可以说明一切
    public void set(T value) {
    	// 该方法用于获取当前线程对象
        Thread t = Thread.currentThread();
        // ThreadLocalMap 是 ThreadLocal 的内部类,这让每个线程都有一个 map
        ThreadLocalMap map = getMap(t);
        // map 没有得到,创建一个当前线程的 map
        if (map != null) {
            map.set(this, value);
        } else {
            // 有 map 则向 map 中存值
            createMap(t, value);
        }
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
		// new ThreadLocalMap 构造函数
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        	// INITIAL_CAPACITY = 16,private Entry[] table; table 为散列表
            table = new Entry[INITIAL_CAPACITY];
            // 哈希函数
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            // 扩容阈值,散列表中的值超过这个数会触发扩容
            setThreshold(INITIAL_CAPACITY);
        }
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

get 方法如下,如果原先 ThreadLocalMap 中没有值会返回 null,ThreadLocalMap 中的 key 是 ThreadLocal 对象本身,值是我们调用接口存入的值

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
1
2
3
4
5
6
7
8
9
10
11
12
13

存入 map 中的 entry 继承了弱引用

        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

因此 Entry 可以转换为:

        static class Entry {
            Object value;
            private ThreadLocal referent;

            Entry(ThreadLocal<?> k, Object v) {
                referent = k;
                value = v;
            }
        }
1
2
3
4
5
6
7
8
9

# 为啥要使用弱引用

让一个弱引用当值主要是为了防止内存泄漏,当 ThreadLocal 需要被回收的时候,如果在 map 中的键是强引用,那么这个对象是无法被回收掉的。即使 ThreadLocal 变量生命周期结束了,设置成 null 了,但如果这个 ThreadLocalMap 中的 Entry 对 ThreadLocal 还是强引用,此时,这个 ThreadLocal 是不会被销毁的

当然将 ThreadLocal 设置成 static 则是例外,此时它被存放在方法区里

所以 jdk 使用弱引用解决了 Entry 的 key 强引用 ThreadLocal 导致 ThreadLocal 无法回收的问题

就算这么做还是有问题,因为 ThreadLocalMap 的键为弱引用,值为强引用,当所有的线程都没有引用这个对象并且发生 GC 后,键的指向都为 null,值的指向对象依然没有被回收,产生了内存泄漏问题

java 为了处理这种问题,定义了方法 replaceStaleEntry,如果系统中 ThreadLocal 变量,调用了它的 get、set 或 remove,三个方法中的任何一个方法,都会自动触发清理机制,将 key 为 null 的 value 值清空。如果 key 和 value 都是 null,那么 Entry 对象会被 GC 回收。如果所有的 Entry 对象都被回收了,属于线程的 ThreadLocalMap 也会被回收了

但是就算 java 做了这样的处理,也是有可能发生内存泄漏问题的:

1,如果 ThreadLocal 被回收后,一直没有其他的 ThreadLocal 调用 get、set 或 remove 方法,就一定会存在 value 的引用 2,ThreadLocal 如果定义为 static,ThreadLocalMap 是一个线程一个 map。请求进来产生多个线程,线程使用 ThreadLocal,线程结束下一个线程进来。这样的话,ThreadLocal 一直有强引用,无用线程的 ThreadLocalMap 也回收不掉

所以在开发的时候,需要使用完 ThreadLocal 之后习惯性的调用 ThreadLocal 对象的 remove 方法(本身来说,直接用 ThreadLocal = null 这种用法就是错误的,应当使用 remove 方法来清除数据)

同时 ThreadLocal 一般会设置为 static,用户请求进来,用一个线程池中线程处理,处理完毕后如果不清理,这个线程可能会处理其他的请求,这时候再调用 ThreadLocal 获取数据,此时会出现问题

那么我们回头来看看 hashMap,弱引用这么好,为什么 hashMap 不使用弱引用优化一下 key 呢?因为没必要,我们不可能使用 map = null 这种语句来删除 key,我们都是调用 remove 方法,因此根本不可能发生内存泄漏

# 碰撞处理

ThreadLocalMap 类似 hashmap,但是所使用的 hash 函数、碰撞处理等方法大不相同

碰撞处理使用动态寻址法,当哈希碰撞发生时,从发生碰撞的那个单元起,按照一定的次序,从哈希表中寻找一个空闲的单元,然后把发生冲突的元素存入到该单元。这个空闲单元又称为开放单元或者空白单元

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    //判断Entry对象如果不为空,则一直循环
    while (e != null) {
        ThreadLocal<?> k = e.get();
        //如果当前Entry的key正好是我们所需要寻找的key
        if (k == key)
            //说明这次真的找到数据了
            return e;
        if (k == null)
            //如果key为空,则清理脏数据
            expungeStaleEntry(i);
        else
            //如果还是没找到数据,则继续往后找
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

为什么不和 hashMap 一样使用列表法呢?它定义了一个 Entry[] 做散列表,最直观的原因是数据量不会像 hashmap 一样,它的数据量少,因此可以用简单的方法实现,多数线程只有少量 ThreadLocal 变量。并且纯数组结构比链表加节点更易于垃圾回收

在 ThreadLocalMap.set() 方法的最后,如果执行完启发式清理工作后,未清理到任何数据,且当前散列数组中 Entry 的数量已经达到了列表的扩容阈值(len*2/3),就开始执行 rehash() 逻辑,也就是扩容处理

// 扩容的入口
private void rehash() {
    // 1. 先全量清理一遍陈旧的Entry(key为null的)
    expungeStaleEntries();
    
    // 2. 清理后如果 size >= threshold × 3/4,则扩容
    // 注意:这里使用 3/4,不是 2/3!
    if (size >= threshold - threshold / 4)
        resize();
}

// 真正的扩容方法
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;  // 双倍扩容
    
    Entry[] newTab = new Entry[newLen];
    int count = 0;
    
    // 3. 遍历旧数组,重新哈希到新数组
    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                // key已被GC回收,value置null帮助GC
                e.value = null; 
            } else {
                // 重新计算位置:使用新的长度
                int h = k.threadLocalHashCode & (newLen - 1);
                
                // 线性探测找空位(ThreadLocalMap没有链表/红黑树!)
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                    
                newTab[h] = e;
                count++;
            }
        }
    }
    
    // 4. 更新数据
    setThreshold(newLen);  // 新阈值 = newLen * 2/3
    size = count;
    table = newTab;
}
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
#ThreadLocal
最后更新: 1/17/2026, 2:51:21 AM
AQS 组件
Redis 集群

← AQS 组件 Redis 集群→

最近更新
01
vibe coding 最佳实践
02-24
02
立直麻将牌效益理论
02-23
03
伪静态是什么
02-08
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式