Netty 作为高性能框架,对 JDK 中的很多类都进行了封装了和优化,Netty 使用了
FastThreadLocalRunnable
对所有
DefaultThreadFactory
创建出来的
Runnable
都进行了包装。 netty的
FastThreadLocal
和
FastThreadLocalThread
的实现相较于
Thread
和
ThreadLocal
不再发生内存泄漏,据说读性能是 JDK 的 5 倍左右,写入的速度也要快 20% 左右。
ThreadLocal
有人叫它线程本地变量,也叫做线程本地存储。和线程同步机制大有不同,同步采用synchronized关键字和J.U.C中的Lock对象来实现,而加锁的目的是为了能让多个线程安全的共享一个变量,
ThreadLocal
为每个线程创建了自己独有的变量副本,采用空间换时间思想。
一个
ThreadLocal
只能存储一个Object对象,如果需要存储多个Object对象那么就需要多个
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
,而
ThreadLocalMap
是
ThreadLocal
实现线程隔离的精髓。
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);//key被传递WeakReference中
value = v;
}
}
public class WeakReference<T> extends Reference<T> {
public WeakReference(T referent) {
super(referent);
}
}
而
ThreadLocalMap.Entry
实现了实现
<k,v>
存储,并继承
WeakReference
类(弱引用),gc时
ThreadLocal
(ThreadLocalMap中的Entry的key)会进行回收的。
强引用:普通的引用,强引用指向的对象不会被回收
软引用:仅有软引用指向的对象,只有发生gc且内存不足,才会被回收
弱引用:仅有弱引用指向的对象,只要发生gc就会被回收
即便是弱引用也绝非完美,以下2中情况视为泄漏:
-
线程的生命周期很长,当
ThreadLocal
没有被外部强引用的时候就会被GC回收,会发生:
ThreadLocalMap
会出现一个
key
为
null
的
Entry
,但这个
Entry
的
value
将永远没办法被访问到(后续在也无法操作set、get等方法了)。如果当这个线程一直没有结束,那这个
key
为
null的
Entry
因为也存在强引用(Entry.value),而
Entry
被当前线程的
ThreadLocalMap
强引用(Entry[] table),导致这个Entry.value永远无法被GC,造成内存泄漏 - 在线程池的场景,程序不停止,线程基本不会销毁
针对第一种情况:
key为null的value问题好多,怎么破?
虽然在
ThreadLocalMap
的设计中,已经考虑到这种情况的发生,它提供cleanSomeSlots()和expungeStaleEntry()方法都能清除key为null的value,ThreadLocal的触发点也很特别:set()、get()、remove()方法中都会调用它们。
也有不完美的地方(被动清除的方式并不是在所有情况下有效):
-
如果
ThreadLocal
的
set()
,
get()
,
remove()
方法没有被调用,就会导致
value
的内存泄漏,时刻预防发生gc - 用static修饰的ThreadLocal,导致ThreadLocal的生命周期和持有它的类一样长,由于ThreadLocal有强引用在,意味着这个ThreadLocal不会被GC。这种情况下,如果不手动删除,Entry的key永远不为null,弱引用就失去了意义,那么执行remove的时候就可以正确进行定位到并且删除。
针对第二种情况:
线程池
使用线程池时归还线程之前记得清除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));
}
回收机制
提供了三种回收机制:
-
自动,执行一个被
FastThreadLocalRunnable
wrap的
Runnable
任务,在任务执行完毕后会自动进行
FastThreadLocal
的清理 -
手动,
FastThreadLocal
和
InternalThreadLocalMap
都提供了remove方法,在合适的时候用户可以(有的时候也是必须,例如普通线程的线程池使用
FastThreadLocal
)手动进行调用,进行显示删除 -
自动,为当前线程的每一个
FastThreadLocal
注册一个Cleaner,当线程对象不强可达的时候,该Cleaner线程会将当前线程的当前ftl进行回收
netty推荐使用前两种方式,第三种方式需要另起线程,耗费资源,而且多线程就会造成一些资源竞争。