Java核心技术之ThreadLocal
作者:互联网
Java核心技术之ThreadLocal
概述
ThreadLocal 提供一种访问某个变量的特殊方式: 访问到的变量属于当前线程,它保证每个线程的变量相互隔离,而同一个线程在任何时候、任何地点都能获取属于本线程的变量。如果要使用 ThreadLocal,通常定义为 private statis
类型。ThreadLocal 适用在多线程的场景范围。
ThreadLocal 用途可总结为两点:
- 保存线程上下文信息,在任意时刻、任意地点可以获取安全的变量值。
- 线程安全,不需要额外的锁保证变量的一致性。
ThreadLocal 在开源项目中的应用有哪些?
- Spring 事务管理。源码详见 org.springframework.transaction.support.TransactionSynchronizationManager。
- 日志框架中的 MDC 利用 ThreadLocal 实现。
局限性:
ThreadLocal 无法解决共享对象的更新问题。因此建议使用 static
修饰,这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量。(引用阿里巴巴编程规范)。
ThreadLocal Demo
// 创建一个「ThreadLocal」对象
private static final ThreadLocal<String> THREAD_LOCAL_NAME =
ThreadLocal.withInitial(() -> Thread.currentThread().getName());
public static void main(String[] args) {
// 从「ThreadLocal」获取本地线程缓存变量值
System.out.println(THREAD_LOCAL_NAME.get());
for (int i = 0; i < 10; i++) {
// 打印
new Thread(() -> System.out.println("threadName: " + THREAD_LOCAL_NAME.get()), "thread-" + i).start();
}
}
// Result
// main
// threadName: thread-0
// threadName: thread-1
// threadName: thread-2
// threadName: thread-3
// threadName: thread-4
// threadName: thread-5
// threadName: thread-6
// threadName: thread-7
// threadName: thread-8
// threadName: thread-9
ThreadLocal API
ThreadLocal API 并不复杂,核心的 API 就只有三个,分别是 get()、set() 和 remove()。ThreadLocal 支持泛型参数,因此,你不需要强转返回值。
InheritableThreadLocal 是 TheadLocal 的一个子类,提供一种线程之间的属性继承关系。比如线程 A 创建线程 B,线程 B 可以拥有访问线程 A 创建的所有的 ThreadLocal 变量。
ThreadLocalMap
ThreadLocalMap 底层采用数组存储数据,内部 Entry 继承 WeakReference 弱引用对 ThreadLocal 进行包装。当 ThreadLocal 外部没有任何强引用指向它时,GC 就会将其回收。但是这会导致内存泄漏,后面再说。ThreadLocalMap 所定义的变量解释如下:
ThreadLocalMap 是一种使用线性探测法实现的哈希表,它是如何解决哈希冲突呢?
我们先认识一下线性探测法。
线性探测法
用大小为 M 的数组保存 N 个键值对,其中 M>N。我们需要依靠数组中的空位解决碰撞冲突。基于这种策略的所有方法被统称为开放地址散列表。开放地址散列表中最简单的方法叫做线性探测法: 当碰撞发生时(当一个键的散列值已经被另一个不同的键占用),我们直接检测散列表的下一个位置(将索引加一)。这样的线性探测可能会产生三种情况:
- 命中,该位置的键和被查找的键相同;
- 未命中,键为空(该位置没有键);
- 继续查找,该位置的键和被查找的键不同。
我们用散列函数找到键在数组中的索引,检查其中的键和被查找的键是否相同。如果不同则继续查找(将索引增大,达到数组结尾时折回数组的开头),直到找到该键或者遇到一个空元素。开放地址散列表的核心思想是与其将内存用作链表,不如将它们作为在散列表的空元素。这此空元素可以作为查找结束的标志(以上引用自算法第4版)。
线性探测是计算机程序解决散列表冲突时所采取的一种策略。散列表这种数据结构用于保存键值对,并且能通过给出的键来查找表中对应的值。线性探测这种策略是在1954年由Gene Amdahl, Elaine M. McGraw,和 Arthur Samuel 所发明,并且最早于1963年由Donald Knuth对其进行分析。 当散列函数对一个给定值产生一个键,并且这个键指向散列表中某个已经被另一个键值对所占用的单元时,线性探测用于解决此时产生的冲突:查找散列表中离冲突单元最近的空闲单元,并且把新的键插入这个空闲单元。同样的,查找也同插入如出一辙:从散列函数给出的散列值对应的单元开始查找,直到找到与键对应的值或者是找到空单元。线性探测能够提供高性能的原因是因为它的良好的引用局部性,然而它与其他解决散列冲突的策略相比对于散列函数的质量更为敏感。
引用维基百科
线性探测算法示意图
开放地址类的散列表的性能依赖公式 α = N/M,表示已被占用空间的比例,一般低于 1/8 到1/2 之间会有较好的性能。
还有一个值得注意的是删除操作,我们不能直接将该键的位置置为 NULL,否则位于此位置之后的元素无法被查找。因此,我们需要将被删除键的右侧所有的键重新插入散列表中。
ThreadLocalMap 如何实现开放地址列表
每个 ThreadLocal 在初始化时都会有一个 threadLocalHashCode 值,而这个值是通过静态方法 nextHashCode() 得到的,这个方法很有趣,每增加一个 ThreadLocal 对象,Hash 值就会固定增加一个魔术值 HASH_INCREMENT(0x61c88647)。实验证明,通过累加魔术值 0x61c88647 生成的 threadLocalHashCode 与 2 的次幂取模,得到的结果可以较为均匀地分布在 2 的次幂大小的数组中(据说与斐波那契散列法以及黄金分割有关)。相关源码解析如下,图文配合理解使用更佳。
ThreadLocal#set
ThreadLocalMap#set
ThreadLocalMap#replaceStaleEntry
expungeStaleEntry
cleanSomeSlots
ThreadLocalMap 的源码还是非常值得阅读的,里面的对性能的把控非常到位。通过局部性全清理和跳跃性探测清理互相配合,平衡性能。
ThreadLocal 内存泄漏 wiki
弱引用是指当一个对象仅仅被弱引用指向,而没有任何强引用指向时,如果这里GC运行,那么这个对象就会被回收,不论当前内存空间是否足够。ThreadLocalMap使用「ThreadLocal」的弱此乃作为key,如果一个 ThreadLocal 没有任何强引用指向时,那么这个 ThreadLocal 势必会被 GC 回收。因此,就会出现 key 为 null 的Entry,就没有办法访问这些 key 为 null 的 Entry 的 value。如果当前线程还未结束,这些 key==null 的 Entry 就会一直存在强引用链,从而无法回收造成内存泄漏。
如何避免 ThreadLocal 内存泄漏
虽然 TheadLocal 在执行 ThreadLocal#set() 或 ThreadLocal#get() 方法时会清除 ThreadLocalMap 中 key==NULL 的 Entry 对象,从而避免内存泄漏。但最佳实践还是应该每次使用完 ThreadLocal,调用它的 remove() 方法清除数据,而不是 set(null),这可能会导致内存泄漏。
我的公众号
参考
- Java ThreadLocal(jenkov.com)
- 手撕ThreadLocal(匠心零度)
- 深入分析 ThreadLocal 内存泄漏问题
- 算法(第4版)
- 拉勾教育-Netty核心原理剖析与RPC实战
标签:Java,thread,ThreadLocalMap,核心技术,threadName,列表,ThreadLocal,线程 来源: https://blog.csdn.net/ClarenceZero/article/details/113094827