JUC_ThreadLocal
视频地址
作用
提供线程内局部变量,不同线程之间不会相互干扰。
ThreadLocal 实例通常来说都是
private static
修饰的,用于关联线程和线程的上下文。减少同一个线程内的函数 或 组件之间传递变量的复杂性。
1 | 线程并发:多线程并发场景下,即单线程场景下用不上。 |
基本使用
方法 | 描述 |
---|---|
new ThreadLocal |
创建 ThreadLocal 对象 |
public void set(T value); | 设置当前线程的绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
ThreadLocal
是 Java 中的一个类,它提供了线程局部(thread-local)变量的能力。这些变量不同于它们的正常变量,因为每一个访问这个变量的线程都有其自己的独立初始化的变量副本。因此,通过 ThreadLocal
实例化的对象,只能被当前线程访问,其他线程无法访问和修改。
以下是 ThreadLocal
的基本使用步骤:
- 创建 ThreadLocal 实例
首先,你需要创建一个 ThreadLocal
的实例。你可以通过无参数的构造函数或提供初始值的构造函数来创建。
1 | // 创建一个没有初始值的 ThreadLocal |
- 设置线程局部变量的值
然后,你可以在你的线程中设置这个变量的值。这通常是通过调用 set(T value)
方法来完成的。
1 | java复制代码 |
- 获取线程局部变量的值
在你的线程中,你可以通过调用 get()
方法来获取这个变量的值。
1 | java复制代码 |
- 清理线程局部变量的值
在不再需要线程局部变量时,你应该清理它的值。这可以通过调用 remove()
方法来完成。但是,通常情况下,如果线程即将结束,那么它的所有 ThreadLocal
变量都会被自动清理。
1 | java复制代码 |
- 注意事项
- 使用
ThreadLocal
时要特别注意内存泄漏问题。如果一个线程长时间存活(比如线程池中的线程),并且ThreadLocal
对象引用了大对象或大量对象,那么这些对象将不会被垃圾回收,从而导致内存泄漏。为了避免这种情况,你应该在不再需要ThreadLocal
变量时调用remove()
方法。 ThreadLocal
不应该用于跨线程传递数据或进行线程间的通信。它主要用于保存线程自己的状态信息。ThreadLocal
是非线程安全的。每个线程都有自己的ThreadLocal
变量副本,因此不需要额外的同步措施。但是,如果你在一个线程中修改了ThreadLocal
变量的值,并期望其他线程看到这个修改,那么ThreadLocal
将无法满足你的需求。
这就是 ThreadLocal
的基本使用方法。它对于保存线程特定的状态信息非常有用,但也需要谨慎使用以避免潜在的问题。
场景案例
JDBC事务 + ThreadLocal
事务要求:
为了保证所有操作在一个事务中,一个线程下的
Service
和Dao
层使用Connection
必须是同一个。线程并发的情况下,每个线程只能操作各自的
Connection
。(每个线程的Connection
需要事务隔离)
最佳设计:
ThreadLocal 的内部结构
JDK1.7 之前的设计
JDK1.7 之后的设计
修改设计后的好处
每个Map 存储 Entry 的数量变少:就会尽量避免 Hash 冲突了,这样效率就变高了。
当 Thread 销毁的时候,ThreadLocal 也会随之销毁,减少内存的使用。反观 早先设计,Thread 结束,ThreadLocalMap 依然存在。
ThreadLocal 核心源码解读
以下代码块是上面四个方法的核心逻辑,为了保证思路清晰,ThreadLocalMap 部分暂时不展开讲,下一个知识点详解:
1 | public void set(T value) { |
1 | public T get() { |
1 | public void remove() { |
1 | /** |
⚠️ protected 表示同包,或者不同包的子类可继承、可重写。其既是为了让子类覆盖而设计的。
ThreadLocalMap 核心源码分析
在分析 `ThreadLocal` 方法的时候,我们了解到 `ThreadLocal` 的操作实际上时围绕 `ThreadLocalMap` 展开的。`ThreadLocalMap` 的源码相对比较复杂,我们从以下三个方面进行讨论。
基本结构
`ThreadLocalMap` 是 ThreadLocal 的内部类(很有意思的点,ThreadLocal的内部类,却在 Thread 内部维护),没有实现 Map 接口,用独立的方式实现了 Map 的功能,其内部 Entry 也是独立实现的。
(1) 成员变量
1 | /** |
(2)存储结构
Entry
在ThreadLocalMap
中, 也是使用Entry
来保存K-V
结构数据的。不过Entry
中的Key
只能是ThreadLocal
对象,这点在构造方法中已经定死了。
另外,Entry
继承自WeakReference
,也就是key
(ThreadLocal
) 是弱引用,其目的是为了将ThreadLocal
对象的生命周期 和 线程的生命周期解绑。
1 | static class Entry extends WeakRefernece<ThreadLocal<?>> { |
弱引用和内存泄漏
有些程序员在使用 `ThreadLocal` 的过程中会发现有内存泄漏的情况,就猜测这个内存泄漏跟`Entry`中使用了弱引用的`key`有关,这个理解是不对的。
我们先来回顾这个问题当中涉及的几个名词概念,再来分析问题。
(1)内存泄漏相关概念:
Memory overflow
:内存溢出,没有足够的内存给内存的申请者使用。Memory leak
:内存泄漏指的是程序中已动态分配的堆内存由于某种原因未释放 或者 无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积导致内存溢出。
(2)弱引用相关概念:0
Java 中的引用有四种类型:强、软、弱、虚。当前这个问题主要涉及到强引用 和 弱引用。
强引用(“Strong Reference”):就是我们最常见的普通对象引用,只要还有一个强引用指向对象,就能表明对象还 “活着”,垃圾回收器就不会回收对象。就算是垃圾回收了他也不会回收这种对象。
弱引用(“Weak Reference”):垃圾回收器一旦发现了只有具有弱引用的对象,不管当前存储空间足够与否,都会回收它的内存。
(3)如果 key 是强引用:
假设 ThreadLocalMap 的 key 使用了强引用,那么会出现内存泄漏么?此时 ThreadLocal 的内存图(实线表示强引用)如下:
业务代码中使用了 ThreadLocal ,threadLocal Ref 被回收了。
但是因为 threadLocalMap 的 Entry 强引用了 ThreadLocal,造成 ThreadLocal 无法被回收。
在没有手动删除这个 Entry 以及 CurrentThread 依然运行的前提下,使用有强引用链 thread Ref → current Thread → threadLocalMap → entry , entry 就不会被回收(Entry当中包含了 ThreadLocal 实例和value), 导致 Entry 的内存泄漏。
也就是说:ThreadLocalMap 中的 key 使用了强引用,是无法避免内存泄漏的。
(4)Key 是弱引用:
当 ThreadLocal 中的Key使用了弱引用,会出现内存泄漏么?
同样假设业务代码中使用了 ThreadLocal,且 ThreadLocal Ref 被回收了。
由于 ThreadLocalMap 只持有 ThreadLocal Map 的弱引用,没有任何强引用指向 ThreadLocal实例,所以 threadLocal 就可以顺利被GC回收,此时 Entry 的 key = null.
但是在没有手动删除这个 Entry 以及 Current Thread 依然运行的前提下,在存在强引用链 threadRef → currentThread → threadLocalMap → entry → value , value 不会被回收,而这块 value 永远不会被访问到了,导致 value 的内存泄漏。
也就是说: ThreadLocalMap 的 key 使用了弱引用,也可能导致内存泄漏的问题。
(5)出现内存泄漏的真实原因:
比较以上两种情况,我们就会发现,内存泄漏的发生 跟 ThreadLocalMap 当中的key 是强还是弱引用没有关系,而造成真正的内存泄漏的原因如下:
没有手动删除这个Entry. (table 数组指定位置置为null, 断掉强引用链)
CurrentThread 依然运行。(Current Thread 销毁,则指向 ThreadLocalMap的引用链就断了,进而引发后续链条上全部对象GC)
综上:ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 导致的(如果存在线程池,Thread 复用,则这个 ThreadLocalMap 永远不会销毁掉),如果没有手动删除对应的key,就会导致内存泄漏。
(6)为什么弱引用会导致内存泄漏,还是要使用弱引用呢:
事实上,ThreadLocalMap 中的 set/getEntry 方法中,会对 key 为 null (即 ThreadLocal 为null)进行判断,如果为 null 的话,那么 value 也会置为 null。
这就意味着 使用完 ThreadLocal, CurrentThread 依然运行的前提下,就算忘记调用 remove 方法,弱引用可以比强引用多一层保障:弱引用的 ThreadLocal 会被回收,对应的value在下一次调用 set、get、remove 中任何一个方法的时候会被清除,从而避免内存泄漏。
Hash冲突的解决
hash 冲突的解决是Map中的一个重要内容。我们以hash冲突的解决方案为线索,来研究下ThreadLocalMap 的核心源码。
构造方法:ThreadLocalMap(ThreadLocal<?> firstKey, Object value)
有不明白的地方看下上述 ThreadLocalMap 核心源码部分的当中关于成员属性的描述。
构造函数首先创建一个 16 大小 Entry 数组,然后计算出 firstKey 的索引,存储到 table 中,并设置 size 和 threshold;
1 | ThreadLocalMap(ThreadLocal<?> firstKey, Object value) { |
重点分析:int i = firstKey.threadLocalHashCode & ( INITIAL_CAPACITY - 1);
这一句中 hash 计算.
(a) 关于threadLocal.threadLocalHashCode
1
2
3
4
5
6
7
8
9
10
11private final threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT); // 追加
}
// AutomicInteger 是一个提供原子操作的 Integer 类, 通过线程安全的方式操作加减,适合高并发的情况下使用.
private static AutomicInteger nextHashCode = new AutomicInteger();
// 特殊的 hash 值:和斐波拉契数列,黄金分割数是有关系的。
private static final int HASH_INCREMENT = 0x61c88647;这里定义了一个 AutomicInteger 类型,每次当前值并加上 HASH_INCREMENT ,这个 HASH_INCREMENT 是和斐波拉契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里,也就是 Entry[] table 中,这样做可以尽量避免 hash 冲突。
(b) 关于 & ( INITIAL_CAPACITY - 1)
计算索引位置的时候 采用了 hashcode & (size - 1) 的方式,相当于取余操作 hashCode % size 的一个更高效的实现,正是因为这种算法,我们要求 size 必须是 2 的整数次幂,这也能保证在索引不越界的情况下,使得 hash 发生冲突的次数减小。
ThreadLocalMap 的 set 方法:
1 | private void set(ThreadLocal<?> key, Object value) { |
执行流程简述:
首先根据 key 计算索引 i, 然后查找 i 位置的上 Entry。
若是 Entry 已经存在,并且 key 等于传入的 key,那么这个时候直接给这个 Entry 赋新的 value 值。
若是 Entry 已经存在,并且 key 为 null,则调用的 replaceStaleEntry 来更换这个 key 为空 Entry
不断循环检测,直到遇见为 null 的地方,这个时候要是还没在循环中 return,那么就在这个 null 的位置重新建一个Entry,并且插入,size + 1。
最后调用 cleanSomeSlots,清理 key 为 null 的Entry,最后返回是否清理了 Entry,接下来判断 sz 是否 ≥ threshold 达到 rehash 条件,达到的话就会调用 rehash 函数执行一次全表的清理工作。
重点分析:ThreadLocalMap 使用 线性探测法 来解决 hash 冲突:
该方法一次探测一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。
举个例子:假设当前的 tab 长度为 16,也就是说如果计算出来 key 的索引为 14,如果 table[14] 上已经有值,并且 key 与当前 key 不一致,那么就发生了hash冲突,这个时候将 14 + 1 得到 15,取 table[15] 进行判断,这个时候如果还是冲突就会返回 0 ,取 table[0],依次类推,直到可以插入。按照上面这个描述,可以把 Entry[] table 看成是一个环形数组。
你了解ThreadLocal变量吗
通常情况下,我们创建的变量是可以被任何⼀个线程访问并修改的。如果想实现每⼀个线程都有 ⾃⼰的专属本地变量该如何解决呢? JDK 中提供的 ThreadLocal 类正是为了解决这样的问题。 ThreadLocal 类主要解决的就是让每个线程拥有只属于自己的专属本地变量,**如果你创建了⼀个 ThreadLocal 类形象的⽐喻成 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的本地副本 **,这也是 ThreadLocal 变量名的由来。他们可以使⽤ **get 和 set **⽅法来获取默认值 或将其值更改为当前线程所存的副本的值,从⽽避免了线程安全问题