JDK的ThreadLocal
ThreadLocal 是 Java 中用于线程局部变量的工具,它允许线程拥有自己的局部变量副本,这些副本对其他线程是不可见的。ThreadLocal 的设计初衷是简化线程间的数据隔离问题,使得每个线程可以独立地拥有和操作自己的数据副本,而无需担心线程安全问题。
ThreadLocal 的实现原理
-
ThreadLocal 类:ThreadLocal 类提供了
set()
,get()
,remove()
等方法,用于设置、获取和删除线程局部变量。 -
Thread 类:Java 的 Thread 类内部维护了一个 ThreadLocalMap 类型的成员变量,用于存储线程局部变量。
-
ThreadLocalMap 类:ThreadLocalMap 是一个特殊的哈希表,它使用线性探测法解决哈希冲突,并且它的键是 ThreadLocal 对象的弱引用,而值是线程局部变量的强引用。
ThreadLocalMap 的结构
-
Entry 类:ThreadLocalMap 的一个内部类,每个 Entry 对象包含一个 key(ThreadLocal 对象的弱引用)和一个 value(线程局部变量)。
-
哈希表:ThreadLocalMap 使用一个数组来存储 Entry 对象,数组的大小通常是 2 的幂次。
-
线性探测法:当发生哈希冲突时,ThreadLocalMap 会通过线性探测法找到下一个空闲位置。
ThreadLocal 的内存泄漏问题
由于 ThreadLocalMap 的键是弱引用,而值是强引用,如果 ThreadLocal 对象被垃圾回收,但是 ThreadLocalMap 中的值仍然被引用,就会造成内存泄漏。为了避免这种情况,ThreadLocal 提供了 remove()
方法,可以在不再需要 ThreadLocal 时显式地清理。
Netty的FastThreadLocal
ThreadLocal 通过 ThreadLocalMap 来存储线程相关的数据,但是 ThreadLocalMap 采用线性探测法解决 Hash 冲突,这在多线程环境下会导致性能问题。FastThreadLocal 通过引入 InternalThreadLocalMap 来优化这一问题
-
InternalThreadLocalMap 的设计原理:InternalThreadLocalMap 采用了一种不同于 ThreadLocalMap 的存储方式,它使用一个 Object 数组来存储数据,而不是使用 Entry 数组。这种设计避免了线性探测法解决 Hash 冲突的问题,从而提高了性能。
-
数组索引的优化:InternalThreadLocalMap 使用一个原子类
AtomicInteger
来保证数组索引的顺序递增。这意味着每个线程都有一个独立的数组索引,从而避免了在多线程环境下可能出现的冲突。 -
FastThreadLocal 的存储方式:FastThreadLocal 使用 Object 数组的前一个位置来存储一个 Set<FastThreadLocal<?>> 集合,而从数组的下一个位置开始,每个位置直接存储对应的 value 数据。这种设计避免了使用 ThreadLocal 的键值对形式,从而减少了内存的使用,并且提高了读写性能。
-
性能对比:与 ThreadLocal 相比,FastThreadLocal 的设计使得在多线程环境下,每个线程都有自己的数据副本,避免了数据共享带来的性能问题。同时,由于避免了线性探测法,FastThreadLocal 的读写性能得到了显著提升。
FastThreadLocal.set(V value)
-
判断 value 是否为缺省值:首先,
set()
方法会检查传入的value
是否为null
或者是否等于InternalThreadLocalMap.UNSET
。如果是,那么set()
方法会直接调用remove()
方法,移除当前线程的FastThreadLocal
数据。这一步是为了避免在FastThreadLocal
未设置值时进行不必要的操作。 -
获取当前线程的 InternalThreadLocalMap:如果
value
不是缺省值,set()
方法会获取当前线程对应的InternalThreadLocalMap
实例。这一步是 FastThreadLocal 性能优化的关键,因为它确保了每个线程都有自己的InternalThreadLocalMap
实例,从而避免了多线程环境下的冲突。 -
替换 InternalThreadLocalMap 中的数据:获取到
InternalThreadLocalMap
后,set()
方法会将当前线程的FastThreadLocal
数据替换为新的value
。这一步通过原子操作完成,确保了线程安全。
public final void set(V value) {
if (value != InternalThreadLocalMap.UNSET) { // 1. value 是否为缺省值
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get(); // 2. 获取当前线程的 InternalThreadLocalMap
setKnownNotUnset(threadLocalMap, value); // 3. 将 InternalThreadLocalMap 中数据替换为新的 value
} else {
remove();
}
}
-
setKnownNotUnset() 方法:
- 这个方法有两个主要功能:
- 设置数组下标
index
位置的值为value
。 - 将
FastThreadLocal
对象加入到待清理的Set
中。
- 设置数组下标
- 这个方法有两个主要功能:
-
数组下标设置 (
threadLocalMap.setIndexedVariable()
):- 如果
index
小于当前数组indexedVariables
的长度,直接设置数组下标index
位置的值。 - 如果
index
超出数组长度,进行扩容操作expandIndexedVariableTableAndSet()
。
- 如果
-
扩容逻辑 (
expandIndexedVariableTableAndSet()
):- 扩容逻辑与 JDK 的
HashMap
类似,通过位移操作确定新的容量,使其为 2 的幂次方。 - 创建新的数组,将原数组内容拷贝到新数组,空余部分用
UNSET
填充。 - 更新
indexedVariables
为新数组。
- 扩容逻辑与 JDK 的
- 待清理的 Set (
addToVariablesToRemove()
):- 检查数组下标为 0 的元素,如果不存在或为
UNSET
,则创建一个新的Set
并设置到该位置。 - 如果 Set 已存在,则直接添加
FastThreadLocal
对象到 Set 中
- 检查数组下标为 0 的元素,如果不存在或为
FastThreadLocal.get()
get()
方法的实现相对简单,主要是通过 InternalThreadLocalMap
来获取当前线程的 FastThreadLocal
数据。由于每个线程都有自己的 InternalThreadLocalMap
实例,get()
方法能够安全地获取到当前线程对应的 FastThreadLocal
数据,而不会与其他线程的数据发生冲突。在 FastThreadLocal 的实现中,slowGet()
方法是作为对非 FastThreadLocalThread
类型线程调用 get()
方法时的一种备选方案。当线程不是 FastThreadLocalThread
,它就不会包含 InternalThreadLocalMap
,因此需要通过其他方式来获取 InternalThreadLocalMap
。在这种情况下,Netty 使用了一个 ThreadLocal
来存储 InternalThreadLocalMap
的实例,这样即使在普通的线程中,也可以通过这种方式来访问到 InternalThreadLocalMap
。
public final V get() {
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
Object v = threadLocalMap.indexedVariable(index); // 从数组中取出 index 位置的元素
if (v != InternalThreadLocalMap.UNSET) {
return (V) v;
}
return initialize(threadLocalMap); // 如果获取到的数组元素是缺省对象,执行初始化操作
}
public Object indexedVariable(int index) {
Object[] lookup = indexedVariables;
return index < lookup.length? lookup[index] : UNSET;
}
private V initialize(InternalThreadLocalMap threadLocalMap) {
V v = null;
try {
v = initialValue();
} catch (Exception e) {
PlatformDependent.throwException(e);
}
threadLocalMap.setIndexedVariable(index, v);
addToVariablesToRemove(threadLocalMap, this);
return v;
}
具体流程
-
get() 方法:
- 从
InternalThreadLocalMap
获取当前线程的映射。 - 尝试从数组中获取下标
index
位置的值。 - 如果值为
UNSET
,则调用initialize()
方法进行初始化,并设置值。
- 从
-
initialize() 方法:
- 调用用户重写的
initialValue()
方法来创建初始值。 - 将新创建的值设置到数组的
index
位置。 - 将
FastThreadLocal
对象加入到待清理的 Set 中。
- 调用用户重写的
总结
FastThreadLocal 之所以快,主要得益于其设计和实现方式,与 JDK 中的 ThreadLocal 相比,FastThreadLocal 在性能上做了一些优化。
-
空间换时间:FastThreadLocal 通过预先分配一个足够大的数组(indexedVariables),并为每个 FastThreadLocal 分配一个唯一的索引,从而避免了 ThreadLocalMap 中的哈希表查找和线性探测法解决冲突的过程。这种方式牺牲了一些内存空间,换取了更快的访问速度。
-
避免哈希冲突:由于 FastThreadLocal 使用的是数组索引直接访问,不存在哈希冲突的问题,因此不需要像 ThreadLocalMap 那样处理冲突,这减少了处理冲突所需的计算。
-
简化数据结构:FastThreadLocal 不需要维护键值对映射,而是直接通过索引访问数组中的数据。这种简化减少了内存占用,并提高了访问速度。
-
减少锁的开销:FastThreadLocal 通过每个线程维护自己的 InternalThreadLocalMap 实例,避免了多线程环境下对共享 ThreadLocalMap 的并发访问,从而减少了锁的开销。
-
原子操作:FastThreadLocal 使用 AtomicInteger 来安全地生成递增的索引,这种方式比 synchronized 同步块或者 Lock 更轻量级,减少了线程间的竞争。
-
内存对齐优化:FastThreadLocal 通过 UnpaddedInternalThreadLocalMap 类来避免内存对齐导致的内存浪费,这有助于提高缓存的利用率。
-
清理机制:除了显式地调用remove方法。在线程池场景中,Netty 通过
FastThreadLocalRunnable
类封装了任务的执行,以确保在任务执行完毕后,能够清理所有注册的FastThreadLocal
对象。
通过上述优化,FastThreadLocal 能够提供比 JDK 的 ThreadLocal 更快的读写性能,特别是在高并发的场景下,这种性能优势更加明显。然而,需要注意的是,FastThreadLocal 仍然需要正确管理,以避免潜在的内存泄漏问题,例如在不再需要 FastThreadLocal 时,应该调用 remove()
方法来清理数据。
那么FastThreadLocal一定更快吗
FastThreadLocal 也有其局限性:
-
普通线程:
- 对于普通的 Java 线程(不是 FastThreadLocalThread 类型的线程),FastThreadLocal 可能不会提供性能优势,甚至可能因为额外的逻辑而变慢。
-
简单操作:
- 如果操作非常简单,ThreadLocal 的同步开销可能微不足道,这时使用 FastThreadLocal 可能不会带来显著的性能提升。
-
内存占用:
- FastThreadLocal 通过预先分配数组来存储线程局部变量,这可能会导致在某些情况下内存占用较高。