FastThreadLocal 从字面意义上来看,它是“Fast” “ThreadLocal”的结合体,寓意为快速的 ThreadLocal。那么,问题来了,Netty 为什么要再造一个 FastThreadLocal?FastThreadLocal 运行快的原因是啥?除了快之外,它还有其他优势吗?
1.先从ThreadLocal说起
ThreadLocal 线程本地变量,每个线程都拥有一份该变量的独立副本,即使是在多线程环境下,每个线程也只能修改和访问自己的那份副本,从而避免了线程安全问题,实现了线程间的隔离。
ThreadLocal 底层是使用 ThreadLocalMap 实现的,这点从 JDK 的源码中可以看出,核心源码如下:
代码语言:javascript复制java复制代码private void set(Thread t, T value) {
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
从 ThreadLocal 的 set 方法可以看出,ThreadLocal 是存储在 ThreadLocalMap 中的,咱们继续看 ThreadLocalMap 的源码实现:
从上面源码可以看出,ThreadLocalMap 中存放的是 Entry(哈希桶),而 Entry 中的 key 就是 ThreadLocal,而 value 则是要存储的值,所以我们得出 ThreadLocal 的底层实现如下:
2.ThreadLocal存在的问题
2.1 性能问题
因为 ThreadLocal 底层是使用 ThreadLocalMap 实现的,ThreadLocalMap 类似于哈希表。当一个线程是拥有多个 ThreadLocal 时,ThreadLocalMap 很容易发生 Hash 冲突,此时 ThreadLocal 就不得不使用线性探测法来解决哈希冲突了,而在解决 Hash 冲突时需要不停地向下寻找,效率较低,因此 ThreadLocal 存在的第一个问题就是性能较低。
2.2 内存泄漏问题
ThreadLocal 也存在内存泄漏的问题,具体来说 ThreadLocalMap 使用 ThreadLocal 对象作为键(Key),并且这个键是弱引用(WeakReference)类型。这意味着当没有其他强引用指向 ThreadLocal 对象时,它将会在下次垃圾回收时被回收。然而,Entry 中保存的值(Value)仍然是强引用,这就可能导致以下问题:
- 弱引用键的回收:一旦外部对 ThreadLocal 实例的所有强引用消失,ThreadLocal 对象本身就会变为弱可达状态。在下一次垃圾回收时,由于是弱引用,ThreadLocal 对象会被回收,但 Entry 中的 Value(即实际存储的数据)仍然是强引用,因此不会被回收。
- Map 引用陷阱:即使 ThreadLocal 键被回收,Entry 仍然存在于 ThreadLocalMap 中,并且由于 Map 对 Entry 的引用,这些 Entry 所持有的 Value 对象也不会被垃圾回收,从而导致这些对象无法被使用也无法被释放,形成了所谓的“内存泄漏”。
- 线程长期存活:在一些场景下,特别是使用线程池时,线程的生命周期往往很长,甚至伴随整个应用的生命周期。这意味着 ThreadLocalMap 中的 Entry 可能会长时间不被清理,进一步加剧了内存泄漏问题。
所以,综合来看,在使用 ThreadLocal 时,如果在使用完之后,未及时调用 remove() 方法的话,就会出现内存泄漏的问题。
3.FastThreadLocal特点
为了解决 ThreadLocal 存在的这些问题,所以 Netty 创造出了一个 FastThreadLocal,FastThreadLocal 的特点如下。
3.1 效率高
FastThreadLocal 之所以性能高的原因是因为其存储结构,在 FastThreadLocal 中并没有向 ThreadLocal 那样,使用哈希表来存储元素,而是使用了数组来进行元素存储,它的核心实现源码如下:
代码语言:javascript复制java复制代码public class FastThreadLocal<V> {
// FastThreadLocal中的index是记录了该它维护的数据应该存储的位置
// InternalThreadLocalMap数组中的下标, 它是在构造函数中确定的
private final int index;
public InternalThreadLocal() {
index = InternalThreadLocalMap.nextVariableIndex();
}
// 省略其他代码
}
FastThreadLocal 核心类 InternalThreadLocalMap 的实现源码如下:
代码语言:javascript复制java复制代码public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap {
// 自增索引, ⽤于计算下次存储到Object数组中的位置
private static final AtomicInteger nextIndex = new AtomicInteger();
private static final int ARRAY_LIST_CAPACITY_MAX_SIZE = Integer.MAX_VALUE - 8;
public static int nextVariableIndex() {
int index = nextIndex.getAndIncrement();
if (index >= ARRAY_LIST_CAPACITY_MAX_SIZE || index < 0) {
nextIndex.set(ARRAY_LIST_CAPACITY_MAX_SIZE);
throw new IllegalStateException("too many thread-local indexed variables");
}
return index;
}
// 省略其他代码
}
从上述源码可以看出,FastThreadLocal 在初始化的时候分配一个数组索引 index,index 的值采用原子类 AtomicInteger 保证顺序递增,通过调用 InternalThreadLocalMap.nextVariableIndex() 方法获得。然后在读写数据的时候通过数组下标 index 直接定位到 FastThreadLocal 的位置,时间复杂度为 O(1)。如果数组下标递增到非常大,那么数组也会比较大,所以 FastThreadLocal 是通过空间换时间的思想提升读写性能。
因此,在 FastThreadLocal 中并不需要使用线性探测法来解决 Hash 冲突,因为它是使用数组进行存储的,每次使用下标进行查询即可,它的查询时间复杂度也是 O(1) 的,所以它的操作效率很高。
3.2 安全性更高
JDK 原生的 ThreadLocal 使用不当可能造成内存泄漏,只能等待线程销毁。然而 FastThreadLocal 却不存在这个问题,在 FastThreadLocal 中不仅提供了 remove() 方法可以主动清除对象,而且它还封装了 FastThreadLocalRunnable,FastThreadLocalRunnable 在最后使用完之后会自动调用 removeAll() 方法将集合中所有对象清理掉,因此 FastThreadLocal 更安全。
FastThreadLocalRunnable 自动清除对象的实现核心源码如下:
代码语言:javascript复制java复制代码final class FastThreadLocalRunnable implements Runnable {
private final Runnable runnable;
@Override
public void run() {
try {
runnable.run();
} finally {
FastThreadLocal.removeAll();
}
}
static Runnable wrap(Runnable runnable) {
return runnable instanceof FastThreadLocalRunnable
? runnable : new FastThreadLocalRunnable(runnable);
}
}
4.小结
FastThreadLocal 相比于 ThreadLocal 存在以下两个主要优点:
- 性能更高:FastThreadLocal 使用了数组的方式来存储元素,所以它的查询时间复杂度 O(1) 相比于 ThreadLocal 的哈希表操作效率更高。
- 安全性更高:FastThreadLocal 中的 FastThreadLocalRunnable 在最后执行完之后会自动调用 removeAll() 将集合中所有对象都清理掉,可以避免内存泄漏的问题,所以它的安全性更高。