FastThreadLocal要了解下

Netty 作为高性能框架,对 JDK 中的很多类都进行了封装了和优化,Netty 使用了 FastThreadLocalRunnable 对所有 DefaultThreadFactory 创建出来的 Runnable 都进行了包装。 netty的FastThreadLocalFastThreadLocalThread的实现相较于ThreadThreadLocal不再发生内存泄漏,据说读性能是 JDK 的 5 倍左右,写入的速度也要快 20% 左右。

ThreadLocal

有人叫它线程本地变量,也叫做线程本地存储。和线程同步机制大有不同,同步采用synchronized关键字和J.U.C中的Lock对象来实现,而加锁的目的是为了能让多个线程安全的共享一个变量,ThreadLocal为每个线程创建了自己独有的变量副本,采用空间换时间思想。

{@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID)

用法

java8之前
    private static final ThreadLocal<Integer> integerThreadLocal = new ThreadLocal<Integer>() {
         @Override
         protected Integer initialValue() {
             return 100;
         }
     };

java8中
private static final ThreadLocal<Integer> integerThreadLocal = ThreadLocal.withInitial(() -> 100);

  • get():返回此线程局部变量的当前线程副本中的值
  • initialValue():返回此线程局部变量的当前线程的“初始值”,默认返回null,供子类重写
  • remove():移除此线程局部变量当前线程的值
  • set(T value):将此线程局部变量的当前线程副本中的值设置为指定值

实现原理

一个Thread类中有这样一个成员变量ThreadLocal.ThreadLocalMap,而ThreadLocalMapThreadLocal实现线程隔离的精髓。

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);//key存储的是ThreadLocal本身,而value则是实际存储的值
    else
        createMap(t, value);
}
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);//如果hash 冲突了
}
//循环所有的元素,直到找到 key 对应的 entry,如果发现了某个元素的 key 是 null,顺手调用 expungeStaleEntry 方法清理 所有 key 为 null 的 entry
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}
//ThreadLocalMap的静态内部类
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

ThreadLocalMap.Entry实现了实现<k,v>存储,并继承WeakReference类,gc时判断ThreadLocal是否已不可达。

ThreadLocalMap的key(Entry.referent为ThreadLocal)设计WeakReference这样有什么好处?

试想下,key使用强引用:在当前ThreadLocal没有被外部强引用时,ThreadLocalMapEntry还保持着ThreadLocal的强引用,ThreadLocal不会被GC。如果没有手动删除,并且当前线程结束了,就导致了Entry的内存泄漏。(有点类似用static修饰ThreadLocal的情况)

即便是弱引用也绝非完美:当ThreadLocal没有被外部强引用的时候(比如线程结束)就会被GC回收,会发生:ThreadLocalMap会出现一个keynullEntry,但这个Entryvalue将永远没办法被访问到。如果当这个线程一直没有结束,那这个keynull的Entry因为也存在强引用(Entry.value),而Entry被当前线程的ThreadLocalMap强引用(Entry[] table),导致这个Entry.value永远无法被GC,造成内存泄漏。

强引用:普通的引用,强引用指向的对象不会被回收
软引用:仅有软引用指向的对象,只有发生gc且内存不足,才会被回收
弱引用:仅有弱引用指向的对象(设置为null时),只要发生gc就会被回收

key为null的value问题好多,怎么破?

虽然在ThreadLocalMap的设计中,已经考虑到这种情况的发生,它提供cleanSomeSlots()和expungeStaleEntry()方法都能清除key为null的value,ThreadLocal的触发点也很特别:set()、get()、remove()方法中都会调用它们。

也有不完美的地方(被动清除的方式并不是在所有情况下有效):

  • 如果ThreadLocalset()get()remove()方法没有被调用,就会导致value的内存泄漏
  • 用static修饰的ThreadLocal,导致ThreadLocal的生命周期和持有它的类一样长,意味着这个ThreadLocal不会被GC。这种情况下,如果不手动删除,Entry的key永远不为null,弱引用就失去了意义。

线程池

使用线程池时归还线程之前记得清除ThreadLocalMap,要不然再取出该线程的时候,ThreadLocal变量还会存在。这就不仅仅是内存泄露的问题了,整个业务逻辑都可能会出错。

解决方法参考:override ThreadPoolExecutor#afterExecute(r, t)方法,对ThreadLocalMap进行清理。当然ThreadLocal最好还是不要和线程池一起使用。

FastThreadLocal

了解完jdk本身的ThreadLocal源码,它使用太麻烦了,易出错,性能也不高!netty对此进行了优化重构,并对jdk原生的线程进行了兼容!

FastThreadLocal有很多优点:

  • 使用了单纯的数组操作来替代了ThreadLocal的hash表操作,所以在高并发的情况下速度更快
  • set操作,它直接根据index进行数组set。而ThreadLocal需要先根据ThreadLocal的hashcode计算数组下标,如果发生hash冲突且有无效的Entry时,还要进行Entry的清理和整理操作,不管是否冲突,都要进行一次log级别的Entry回收操作,所以肯定快不了
  • get操作,它直接根据index进行获取。而ThreadLocal需要先根据ThreadLocal的hashcode计算数组下标,然后再根据线性探测法进行get操作,如果不能根据直接索引获取到value的话并且在向后循环遍历的过程中发现了无效的Entry,则会进行无效Entry的清理和整理操作
  • remove操作,它直接根据index从数组中删除当前FastThreadLocal的value,然后从Set集合中删除当前的FastThreadLocal,之后还可以进行删除回调操作(功能增强)。而ThreadLocal需要先根据ThreadLocal的hashcode计算数组下标,然后再根据线性探测法进行remove操作,最后还需要进行无效Entry的整理和清理操作。

缺点也有:

FastThreadLocal较于ThreadLocal不好的地方就是内存占用大,不会重复利用已经被删除(用UNSET占位)的数组位置,只会一味增大,是典型的“空间换时间”的操作。

使用

private static final FastThreadLocal<Integer> fastThreadLocal1 = new FastThreadLocal<Integer>(){
    @Override
    protected Integer initialValue() throws Exception {
        return 100;
    }
    @Override
    protected void onRemoved(Integer value) throws Exception {
        System.out.println(value + ":我被删除了");
    }
};
@Test
public void testSetAndGetByCommonThread() {
    Integer x = fastThreadLocal1.get();
    fastThreadLocal1.remove();
    x = fastThreadLocal1.get();//输入null,而ThreadLocal不同一定是有
}
@Test
public void testSetAndGetByFastThreadLocalThread() {
    new FastThreadLocalThread(()->{
        Integer x = fastThreadLocal1.get();
		fastThreadLocal1.set(200);
    }).start();
}
private static final Executor executor = FastThreadExecutors.newCachedFastThreadPool("test");

@Test
public void testSetAndGetByFastThreadLocalThreadExecutor() {
    executor.execute(()->{
        Integer x = fastThreadLocal1.get();
        String s = fastThreadLocal2.get();
        fastThreadLocal1.set(200);
    });
}

数据结构

对于jdk的ThreadLocal来讲,其底层数据结构就是一个Entry[]数组,key为ThreadLocal,value为对应的值(hash表);通过线性探测法解决hash冲突。

先了解FastThreadLocalThread,每个FastThreadLocalThread内部都有一个InternalThreadLocalMap,而InternalThreadLocalMap 内部存储的key就是FastThreadLocal value就是100(上面的),没错,和ThreadLocal的设计套路大同小异!但是InternalThreadLocalMap 底层是单纯的简单数组Object[],初始length==32,数组的第一个元素index=0存储一个Set<FastThreadLocal<?>>的set集合,存储所有有效的FastThreadLocal

每当有一个FastThreadLocal的value设置到数组中的时候,首先将当前的FastThreadLocal对象添加到Object[0]的set集合中,然后将FastThreadLocal的value存入Object[]的其余位置(除0以外),而位置也很讲究与FastThreadLocal实例属性index对应。

//FastThreadLocal
public V get() {
    // 1、获取InternalThreadLocalMap
    InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
    // 2、从InternalThreadLocalMap获取索引为index的value,如果该索引处的value是有效值,不是占位值,则直接返回
    Object value = threadLocalMap.indexedVariable(index);
    if (value != InternalThreadLocalMap.UNSET) {
        return (V) value;
    }
    // 3、indexedVariables[index]没有设置有效值,执行初始化操作,获取初始值
    V initialValue = initialize(threadLocalMap);
    // 4、注册资源清理器:当该ftl所在的线程不强可达(没有强引用指向该线程对象)时,清理其上当前ftl对象的value和set<FastThreadLocal<?>>中当前的ftl对象
    registerCleaner(threadLocalMap);
    return initialValue;
}
//兼容性
public static InternalThreadLocalMap get() {
    Thread current = Thread.currentThread();
    if (current instanceof FastThreadLocalThread) {
        return fastGet((FastThreadLocalThread) current);
    }
    return slowGet();
}
private static InternalThreadLocalMap fastGet(FastThreadLocalThread current) {
    InternalThreadLocalMap threadLocalMap = current.threadLocalMap();
    if (threadLocalMap == null) {
        threadLocalMap = new InternalThreadLocalMap();
        current.setThreadLocalMap(threadLocalMap);
    }
    return threadLocalMap;
}
/**
 * 兼容非FastThreadLocalThread
 */
private static final ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap = new ThreadLocal<>();
private static InternalThreadLocalMap slowGet() {
    InternalThreadLocalMap threadLocalMap = slowThreadLocalMap.get();
    if (threadLocalMap == null) {
        threadLocalMap = new InternalThreadLocalMap();
        slowThreadLocalMap.set(threadLocalMap);
    }
    return threadLocalMap;
}
private void registerCleaner(InternalThreadLocalMap threadLocalMap) {
    Thread current = Thread.currentThread();
    // 如果已经开启了自动清理功能 或者 已经对threadLocalMap中当前的FastThreadLocal开启了清理线程
    if (FastThreadLocalThread.willCleanupFastThreadLocals(current) || threadLocalMap.isCleanerFlags(index)) {
        return;
    }
    // 设置是否已经开启了对当前的FastThreadLocal清理线程的标志
    threadLocalMap.setCleanerFlags(index);
    // 将当前线程和清理任务注册到ObjectCleaner上去
    ObjectCleaner.register(current, () -> remove(threadLocalMap));
}

回收机制

提供了三种回收机制:

  1. 自动,执行一个被FastThreadLocalRunnable wrap的Runnable任务,在任务执行完毕后会自动进行FastThreadLocal的清理
  2. 手动,FastThreadLocalInternalThreadLocalMap都提供了remove方法,在合适的时候用户可以(有的时候也是必须,例如普通线程的线程池使用FastThreadLocal)手动进行调用,进行显示删除
  3. 自动,为当前线程的每一个FastThreadLocal注册一个Cleaner,当线程对象不强可达的时候,该Cleaner线程会将当前线程的当前ftl进行回收

netty推荐使用前两种方式,第三种方式需要另起线程,耗费资源,而且多线程就会造成一些资源竞争。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章