Netty-FastThreadLocal
ThreadLocal:
ThreadLocalMap中实际存储键值对的是Entry[]数组,Entry对象的Key为ThreadLocal对象,value为线程持有的本地变量。
Entry继承了WeakReference<ThreadLocal<?>>,所以Entry对象是其key即ThreadLocal对象的一个弱引用,这种设计是为了当ThreadLocal对象被置为null时,Entry对象会在第二次YoungGC被回收,线程本地变量value也会被回收。
不足:
①:然而ThreadLocal对象一般是静态的,因此Entry对象不会被GC回收,如果没有在代码中手动 remove的话会导致内存泄露。
②:ThreadLocalMap是使用线性探测的方式解决hash冲突的,如果没有找到空闲的slot,就不断往后尝试,直到找到一个空闲的位置来插入entry,这种方式在经常遇到hash冲突时,影响效率
ThreadLocal博客:https://www.cnblogs.com/yangyongjie/p/10574591.html
FastThreadLocal:
Netty为了提高性能,改进了JDK ThreadLocal。
Netty实现的FastThreadLocal优化了Java原生ThreadLocal的访问速度,存储速度,避免了检测弱引用带来的线程本地变量value垃圾回收的问题,及数组位置冲突带来的线性查找问题。
JDK和Netty类之间对应关系:
JDK | Netty | 说明 |
Thread | FastThreadLocalThread | FastThreadLocalThread是Thread的子类 |
ThreadLocal | FastThreadLocal | FastThreadLocal用于操作FastThreadLocalThread中的InternalThreadLocalMap |
ThreadLocalMap | InternalThreadLocalMap | FastThreadLocalThread中的私有属性 |
FastThreadLocalThread是对JDK Thread类的一层包装,每个线程对应一个InternalThreadLocalMap实例。
public class FastThreadLocalThread extends Thread { // 用于存储线程本地变量,代替ThreadLocalMap private InternalThreadLocalMap threadLocalMap; ... }
对应关系图:
FastThreadLocal使用示例:
import io.netty.util.concurrent.FastThreadLocal; /** * Netty FastThreadLocal保存的本地变量 * 只有在 FastThreadLocalThread 线程中调用才会使用FastThreadLocal的逻辑,否则会使用ThreadLocal * */ public class FastContext { private static FastThreadLocal<FastContext> FAST_THREAD_LOCAL = new FastThreadLocal<FastContext>() { @Override protected FastContext initialValue() throws Exception { return new FastContext(); } }; public static FastContext currentContext() { return FAST_THREAD_LOCAL.get(); } public static void remove() { FAST_THREAD_LOCAL.remove(); } /** * 线程追踪ID */ private String thraceId; public String getThraceId() { return thraceId; } public void setThraceId(String thraceId) { this.thraceId = thraceId; } }
写入本地变量:
public static void main(String[] args) { // Netty提供的DefaultThreadFactory创建的线程是FastThreadLocalThread类型的 DefaultThreadFactory NettyFastThreadFactory = new DefaultThreadFactory("FastThreadTest"); NettyFastThreadFactory.newThread(new Runnable() { @Override public void run() { FastContext fastContext = FastContext.currentContext(); String uuid = UUID.randomUUID().toString(); System.out.println(uuid); // 27688ce3-4786-4197-b072-ade47bd5061a fastContext.setThraceId(uuid); testFastThreadLocal(); } }).start(); } private static void testFastThreadLocal() { FastContext fastContext = FastContext.currentContext(); System.out.println(fastContext.getThraceId()); // 27688ce3-4786-4197-b072-ade47bd5061a FastContext.remove(); }
FastThreadLocal如何解决hash冲突的?
使用Object[]数组来保存本地变量value,每个FastThreadLocal实例对应的value都保存在数组的同一下标中。
①:每一个FastThreadLocal实例创建时,分配一个下标Index(使用AtomicInteger来保证顺序递增),每个FastThreadLocal实例都能获取一个不重复的下标。
②:FastThreadLocal实例保存了其被分配的下标Index,所有使用同一个FastThreadLocal实例保存的本地变量value对应的下标都相同,都是Index(即所有FastThreadLocalThread使用同一个FastThreadLocal实例来保存的本地变量value,都在其线程中InternalThreadLocalMap中Object[]数组的同一下标中)
③:当调用get()方法时,直接根据FastThreadLocal实例保存的index,去Object[]数组中去获取value,时间复杂度为O(1)
如果程序中有多个FastThreadLocal实例,那么数组下标递增到非常大,数组也会比较大。虽然数组较大,但是避免了在ThreadLocal中计算索引下标位置及处理Hash冲突带来的损耗,所以在操作数组时使用固定下标比使用哈希下标有一定的性能优势,特别是在拼单使用时会非常显著,所以 FastThreadLocal 是通过空间换时间的思想提升读写性能
分配数组下标的逻辑:
①:FastThreadLocal 中的常量index,用于保存其在实例化时获取到的递增的数组下标
private final int index; public FastThreadLocal() { index = InternalThreadLocalMap.nextVariableIndex(); }
②:InternalThreadLocalMap
每个FastThreadLocal实例化时,index+1,有多少个FastThreadLocal实例,index就为多大
// 用于生成数组下标 private static final AtomicInteger nextIndex = new AtomicInteger(); // 生成数组下标 public static int nextVariableIndex() { int index = nextIndex.getAndIncrement(); if (index < 0) { nextIndex.decrementAndGet(); throw new IllegalStateException("too many thread-local indexed variables"); } return index; }
③:保存本地变量value时,根据FastThreadLocal中的index将value保存到指定的数组下标中
private void setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) { // 将value保存在当前FastThreadLocalThread的InternalThreadLocalMap的Object[]的index下标中 if (threadLocalMap.setIndexedVariable(index, value)) { addToVariablesToRemove(threadLocalMap, this); } }
问题:
①:递增的index会不会非常大,导致数组也非常大?
不会,每种数据类型的线程本地变量只需要定义一个ThreadLocal即可,所有的线程共享这个ThreadLocal实例,所以线程本地变量的数据类型有几种,index最大就为多大,一般在个位数。
FastThreadLocal部分源码分析:
set():
public final void set(V value) { // 如果value不等于UNSET值,则保存 if (value != InternalThreadLocalMap.UNSET) { // 获取当前线程的InternalThreadLocalMap InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get(); // 将Map中对应下标的数据替换成value setKnownNotUnset(threadLocalMap, value); } else { remove(); } }
get():
public final V get() { // 获取当前线程的InternalThreadLocalMap InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get(); // 根据当前FastThreadLocal的index常量获取对应下标的value Object v = threadLocalMap.indexedVariable(index); // 如果value不等于数组中的UNSET值(可以理解成默认值),则返回value if (v != InternalThreadLocalMap.UNSET) { return (V) v; } // 将initialValue保存到Map的index下标中并返回 return initialize(threadLocalMap); }
总结:
①:FastThreadLocal只在FastThreadLocalThread线程中使用才能发挥其性能,在Thread线程中使用的还是ThreadLocal
②:当对一个线程设置的线程本地变量不多的情况下,ThreadLocal的性能比FastThreadLocal高,所以一般情况下,使用ThreadLocal足矣
END.