本文主要是介绍震惊:Netty竟然对ThreadLocal做出这种事...,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
目录
1、ThreadLocal
ThreadLocal内存泄露?
既然是ThreadLocal的弱引用导致了内存泄漏,那为什么不使用强引用?
最佳实践
源码
2、FastThreadLocal
优化1:不需要手动remove
优化2:利用字节填充避免伪共享问题
优化3:使用常量下标在数组中定位元素替代ThreadLocal通过哈希和哈希表
源码
1、ThreadLocal
ThreadLocal类提供了线程局部 (thread-local) 变量。这些变量与普通变量不同,每个线程都可以通过其 get 或 set方法来访问自己的独立初始化的变量副本。
Thread类中包含ThreadLocalMap对象,ThreadLocalMap是ThreadLocal的静态内部类,ThreadLocalMap中的对象都是以ThreadLocal对象作为key存储对应的value。
ThreadLocal内存泄露?
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用,系统GC时这个ThreadLocal就会被回收,ThreadLocalMap中就出现了key为null的Entry,如果线程不结束,这些Entry的value就会存在一条强引用链:Thread -> ThreadLocal.ThreadLocalMap -> Entry[] -> Enrty -> value,造成内存泄漏。
ThreadLocalMap针对此做了一些优化,比如在调用ThreadLocal的get()、set()、remove()时都会清理ThreadLocalMap中所有key为null的value。但不能避免内存泄漏的发生,比如分配使用了ThreadLocalMap后不再调用get()、set()、remove()方法。
java对象的引用包括 : 强引用、软引用、弱引用、虚引用
- 强引用:我们平时一般都是这种引用,当一个对象被一个或一个以上的引用变量所引用时,它处于可达状态,不可能被系统垃圾回收机制回收。
- 软引用:软引用需要通过SoftReference类来实现,当系统内存空间足够时,它不会被系统回收,当系统内存空间不够时,系统可能回收它。
- 弱引用:弱引用通过WeakReference类实现,当系统垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存。
- 虚引用:虚引用通过PhantomReference类实现,虚应用完全类似于没有引用。虚引用主要用来跟踪对象被垃圾回收器回收的活动。
既然是ThreadLocal的弱引用导致了内存泄漏,那为什么不使用强引用?
ThreadLocalMap本身并没有为外界提供取出和存放数据的API,我们所能获得数据的方式只有通过ThreadLocal类提供的API来间接的从ThreadLocalMap取出数据,所以,当我们用不了key(ThreadLocal对象)的API也就无法从ThreadLocalMap里取出指定的数据。使用强引用的话,如果引用ThreadLocal的对象已经回收了,我们就无法在get到ThreadLocalMap中的数据,也就是说已经有部分数据无效了,但ThreadLocalMap还持有对ThreadLocal的强引用,引用链Thread -> ThreadLocal.ThreadLocalMap -> Entry[] -> Enrty -> key,当这个线程还未结束时,他持有的强引用,包括递归下去的所有强引用都不会被垃圾回收器回收,导致Entry内存泄漏。
比如下面这个例子,当Test对象被回收时,没法通过get方法使用ThreadLocalMap中的数据了,那保存数据的Entry对象就没用了,所以要想办法让系统自动回收对应的Entry对象。
但是让Entry对象或其中的value对象做为弱引用都是非常不合理的。所以,让key(threadLocal对象)为弱引用,自动被垃圾回收,key就变为null了,下次,我们就可以通过Entry不为null,而key为null来判断该Entry对象该被清理掉了。
public class ThreadLocalTest {public static void main(String[] args) throws InterruptedException {doSomeWork(); //如果给它一个引用,比如Test test = doSomeWork(),System.gc()就不会回收threadLocal,直到方法结束才可能会被回收System.gc();TimeUnit.SECONDS.sleep(1);Thread thread = Thread.currentThread();System.out.println(thread);}private static Test doSomeWork() {Test test = new Test();System.out.println("int value:" + test.get());return test;}
}class Test {private ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);public Integer get() {return threadLocal.get();}
}
最佳实践
- 每次使用完ThreadLocal,调用remove()进行清理。
try {
// 业务逻辑,threadLocal#get, threadLocal#set
} finally {threadLocal.remove();
}
阿里规范:
15.【参考】 ThreadLocal 无法解决共享对象的更新问题, ThreadLocal 对象建议使用 static
修饰。这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享
此静态变量 ,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象 ( 只
要是这个线程内定义的 ) 都可以操控这个变量。
为了避免重复创建与线程相关的变量(thread specific object),一个ThreadLocal实例对应当前线程中的一个TSO实例。如果把ThreadLocal声明为某个类的实例变量(而不是静态变量),那么每创建一个该类的实例就会导致一个新的TSO实例被创建,这些被创建的TSO实例是同一个类的实例。于是,同一个线程可能会访问到同一个TSO(指类)的不同实例,这即便不会导致错误,也会导致浪费(重复创建等同的对象)!因此,一般我们将ThreadLocal使用static修饰即可。
源码
Entry节点,key弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}
HashMap是使用拉链法解决hash冲突的,ThreadLocalMap是使用线性探测解决hash冲突的(内部只维护Entry数组,没有链表)
set方法:
public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}private void set(ThreadLocal<?> key, Object value) {// We don't use a fast path as with get() because it is at// least as common to use set() to create new entries as// it is to replace existing ones, in which case, a fast// path would fail more often than not.Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();if (k == key) {e.value = value;return;}if (k == null) {//更换这个key为空的EntryreplaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value);int sz = ++size;//清理key为null的Entryif (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}
get方法:
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;elsereturn getEntryAfterMiss(key, i, e);}
源码中在清除泄漏的Entry时,会进行rehash,防止数组的当前位置为null后,有hash冲突的Entry访问不到的问题。
remove方法:
private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {if (e.get() == key) {e.clear();expungeStaleEntry(i);return;}}}private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// expunge entry at staleSlottab[staleSlot].value = null;tab[staleSlot] = null;size--;// Rehash until we encounter nullEntry e;int i;for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();if (k == null) {e.value = null;tab[i] = null;size--;} else {int h = k.threadLocalHashCode & (len - 1);if (h != i) {tab[i] = null;// Unlike Knuth 6.4 Algorithm R, we must scan until// null because multiple entries could have been stale.while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}return i;}
2、FastThreadLocal
FastThreadLocal 并不是 JDK 自带的,而是在 Netty 中造的一个轮子,Netty 为什么要重复造轮子呢?
源码中的注释:
FastThreadLocal 是一个特殊的ThreadLocal 变体,当从线程类 FastThreadLocalThread 中访问 FastThreadLocal时可以获得更高的访问性能。
使用示例:
public class FastThreadLocalTest {private static FastThreadLocal<Integer> fastThreadLocal = new FastThreadLocal<>();public static void main(String[] args) throws InterruptedException {new FastThreadLocalThread(() -> {for (int i = 0; i < 100; i++) {fastThreadLocal.set(i);System.out.println(Thread.currentThread().getName() + "====" + fastThreadLocal.get());Thread.sleep(200);}}, "fastThreadLocal1").start();new FastThreadLocalThread(() -> {for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName() + "====" + fastThreadLocal.get());Thread.sleep(200);}}, "fastThreadLocal2").start();}
}
FastThreadLocal和ThreadLocal在用法上面基本差不多,没有什么特别
FastThreadLocal为什么快?
优化1:不需要手动remove
构造FastThreadLocalThread的时候,通过FastThreadLocalRunnable对Runnable对象进行了包装,FastThreadLocalRunnable在执行完之后都会调用FastThreadLocal.removeAll()。
如果使用FastThreadLocal就不要使用普通线程,而应该构建FastThreadLocalThread,若使用普通线程,还是需要手动remove。
public class FastThreadLocalThread extends Thread {public FastThreadLocalThread(Runnable target) {super(FastThreadLocalRunnable.wrap(target));cleanupFastThreadLocals = true;}
}
final class FastThreadLocalRunnable implements Runnable {private final Runnable runnable;private FastThreadLocalRunnable(Runnable runnable) {this.runnable = ObjectUtil.checkNotNull(runnable, "runnable");}@Overridepublic void run() {try {runnable.run();} finally {FastThreadLocal.removeAll();}}static Runnable wrap(Runnable runnable) {return runnable instanceof FastThreadLocalRunnable ? runnable : new FastThreadLocalRunnable(runnable);}
}
优化2:利用字节填充避免伪共享问题
伪共享概念可以参考美团在Disruptor中的解释:高性能队列——Disruptor - 美团技术团队
InternalThreadLocalMap中填充了9个long类型数据,至于为什么是9个,github上也有讨论:About FastThreadLocal Cache line padding ???? · Issue #9284 · netty/netty · GitHub
结论是这可能是个bug,这些填充数据已经标记为了废弃,在新推出的版本5中对InternalThreadLocalMap进行了重构
优化3:使用常量下标在数组中定位元素替代ThreadLocal通过哈希和哈希表
ThreadLocalMap中的Entry数组通过哈希来定位元素,并通过线性探测法解决hash冲突,所以get、set时时间复杂度并非o(1)。
而FastThreadLocal引入了InternalThreadLocalMap这种新的数据结构,内部不再是Entry数组,而是只用来存储value的Object数组。数组下标在创建FastThreadLocal就以CAS的形式分配好了。
public class FastThreadLocal<V> {private final int index;public FastThreadLocal() {index = InternalThreadLocalMap.nextVariableIndex();}public static int nextVariableIndex() {int index = nextIndex.getAndIncrement();if (index < 0) {nextIndex.decrementAndGet();throw new IllegalStateException("too many thread-local indexed variables");}return index;}}
所以在get或set时,只需要根据下标从数组中取就可以了,时间复杂度为o(1)。
原先是拿ThreadLocal对象从Entry数组中进行hash查找,现在变成了直接拿ThreadLocal对象中的数组下标在valur数组中查找。
源码
get方法:
// 先获取InternalThreadLocalMap对象,然后根据FastThreadLocal存储的数组下标去取数据 public final V get() {InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();Object v = threadLocalMap.indexedVariable(index);if (v != InternalThreadLocalMap.UNSET) {return (V) v;}return initialize(threadLocalMap);}
//若线程为FastThreadLocalThread,执行fastGet
//若为普通线程,执行slowGet public static InternalThreadLocalMap get() {Thread thread = Thread.currentThread();if (thread instanceof FastThreadLocalThread) {return fastGet((FastThreadLocalThread) thread);} else {return slowGet();}}//fastGet过程private static InternalThreadLocalMap fastGet(FastThreadLocalThread thread) {InternalThreadLocalMap threadLocalMap = thread.threadLocalMap();if (threadLocalMap == null) {thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap());}return threadLocalMap;}public final InternalThreadLocalMap threadLocalMap() {return threadLocalMap;}private InternalThreadLocalMap() {indexedVariables = newIndexedVariableTable();}private static Object[] newIndexedVariableTable() {Object[] array = new Object[INDEXED_VARIABLE_TABLE_INITIAL_SIZE];Arrays.fill(array, UNSET);return array;}//slowGet过程private static InternalThreadLocalMap slowGet() {InternalThreadLocalMap ret = slowThreadLocalMap.get();if (ret == null) {ret = new InternalThreadLocalMap();slowThreadLocalMap.set(ret);}return ret;}private static final ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap =new ThreadLocal<InternalThreadLocalMap>();
可以看到,如果是普通线程,会借助原生ThreadLocal来实现,将InternalThreadLocalMap对象作为value存储在ThreadLocalMap中,相当于在原来的基础上又套了一层逻辑,所以会降低效率。
因此如果在使用FastThreadLocal时一定要搭配FastThreadLocalThread来使用。
set方法:
public final void set(V value) {if (value != InternalThreadLocalMap.UNSET) {InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();setKnownNotUnset(threadLocalMap, value);} else {remove();}}private void setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) {if (threadLocalMap.setIndexedVariable(index, value)) {addToVariablesToRemove(threadLocalMap, this);}}public boolean setIndexedVariable(int index, Object value) {Object[] lookup = indexedVariables;if (index < lookup.length) {Object oldValue = lookup[index];lookup[index] = value;return oldValue == UNSET;} else {expandIndexedVariableTableAndSet(index, value);return true;}}//扩容为2的n次方private void expandIndexedVariableTableAndSet(int index, Object value) {Object[] oldArray = indexedVariables;final int oldCapacity = oldArray.length;int newCapacity = index;newCapacity |= newCapacity >>> 1;newCapacity |= newCapacity >>> 2;newCapacity |= newCapacity >>> 4;newCapacity |= newCapacity >>> 8;newCapacity |= newCapacity >>> 16;newCapacity ++;Object[] newArray = Arrays.copyOf(oldArray, newCapacity);Arrays.fill(newArray, oldCapacity, newArray.length, UNSET);newArray[index] = value;indexedVariables = newArray;}//addToVariablesToRemove方法会将 FastThreadLocal 对象存放到 threadLocalMap 中的一个集合中
//这个集合用于在需要的时候集中销毁 FastThreadLocalprivate static void addToVariablesToRemove(InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);Set<FastThreadLocal<?>> variablesToRemove;if (v == InternalThreadLocalMap.UNSET || v == null) {variablesToRemove = Collections.newSetFromMap(new IdentityHashMap<FastThreadLocal<?>, Boolean>());threadLocalMap.setIndexedVariable(variablesToRemoveIndex, variablesToRemove);} else {variablesToRemove = (Set<FastThreadLocal<?>>) v;}variablesToRemove.add(variable);}
remove方法:
public final void remove(InternalThreadLocalMap threadLocalMap) {if (threadLocalMap == null) {return;}Object v = threadLocalMap.removeIndexedVariable(index);removeFromVariablesToRemove(threadLocalMap, this);if (v != InternalThreadLocalMap.UNSET) {try {onRemoval((V) v);} catch (Exception e) {PlatformDependent.throwException(e);}}}
这篇关于震惊:Netty竟然对ThreadLocal做出这种事...的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!