震惊:Netty竟然对ThreadLocal做出这种事...

2024-03-17 23:32

本文主要是介绍震惊: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做出这种事...的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/820538

相关文章

线程--(1)ThreadLocal简单使用

一、概念 ThreadLocal概念:线程局部变量,是一种并发线程访问变量的解决方案,与synchronized等加锁不同,ThreadLocal完全不提供锁,而使用空间换取时间的方式,为每一个线程变量提供一个副本,以保证线程之间的安全,因为它们之间是相互独立的。 二、代码说明 package com.flx.king.it_201707;/*** 功能:ThreadLocal的使

J.U.C Review - ThreadLocal原理源码分析

文章目录 一致性问题一致性问题简介解决一致性问题的常见方法 ThreadLocal什么是 ThreadLocalThreadLocal 的 线程模型ThreadLocal 的工作原理使用场景ThreadLocal 的基本 API1. 构造函数 `ThreadLocal()`2. 初始化方法 `initialValue()`3. 访问器 `get()` 和 `set()`4. 回收方法 `re

高效传输秘籍,揭秘Rsync和SCP的优劣,助你做出明智选择!

在日常的运维工作中,文件传输任务频繁出现,而选择合适的工具能显著提高工作效率。Rsync 和 SCP 是两款常见的文件传输工具,但它们各具优缺点,适合不同的场景。本文将通过深入分析这两款工具的特性、使用场景和性能,帮助你做出明智的选择,从而在文件传输中省时省力。 Rsync 与 SCP 简介 Rsync:增量传输的强大工具 Rsync 是一款支持文件同步的工具,广泛应用于备份和传输

【Netty】netty中都是用了哪些设计模式

对于工程师来说,掌握并理解运用设计模式,是非常重要的,但是除了学习基本的概念之外,需要结合优秀的中间件、框架源码学习其中的优秀软件设计,这样才能以不变应万变。 单例模式 单例模式解决的对象的唯一性,一般来说就是构造方法私有化、然后提供一个静态的方法获取实例。 在netty中,select用于处理CONTINUE、SELECT、BUSY_WAIT 三种策略,通过DefaultSelectStra

Java语言的Netty框架+云快充协议1.5+充电桩系统+新能源汽车充电桩系统源码

介绍 云快充协议+云快充1.5协议+云快充1.6+云快充协议开源代码+云快充底层协议+云快充桩直连+桩直连协议+充电桩协议+云快充源码 软件架构 1、提供云快充底层桩直连协议,版本为云快充1.5,对于没有对接过充电桩系统的开发者尤为合适; 2、包含:启动充电、结束充电、充电中实时数据获取、报文解析、Netty通讯框架、包解析工具、调试器模拟器软件等; 源码合作 提供完整云快充协议源代码

多线程 | ThreadLocal源码分析

文章目录 1. ThreadLocal解决了什么问题数据隔离避免参数传递资源管理 2. ThreadLocal和Synchronized3. ThreadLocal核心核心特性常见方法使用场景注意事项 4. ThreadLocal如何实现线程隔离的?(重点)ThreadLocal 的自动清理与内存泄漏问题阿里巴巴 ThreadLocal 编程规约 5. ThreadLocal源码分析Thre

Netty源码解析9-ChannelHandler实例之MessageToByteEncoder

MessageToByteEncoder框架可见用户使用POJO对象编码为字节数据存储到ByteBuf。用户只需定义自己的编码方法encode()即可。 首先看类签名: public abstract class MessageToByteEncoder<I> extends ChannelOutboundHandlerAdapter 可知该类只处理出站事件,切确的说是write事件

Netty源码解析8-ChannelHandler实例之CodecHandler

编解码处理器作为Netty编程时必备的ChannelHandler,每个应用都必不可少。Netty作为网络应用框架,在网络上的各个应用之间不断进行数据交互。而网络数据交换的基本单位是字节,所以需要将本应用的POJO对象编码为字节数据发送到其他应用,或者将收到的其他应用的字节数据解码为本应用可使用的POJO对象。这一部分,又和JAVA中的序列化和反序列化对应。幸运的是,有很多其他的开源工具(prot

Netty源码解析7-ChannelHandler实例之TimeoutHandler

请戳GitHub原文: https://github.com/wangzhiwubigdata/God-Of-BigData TimeoutHandler 在开发TCP服务时,一个常见的需求便是使用心跳保活客户端。而Netty自带的三个超时处理器IdleStateHandler,ReadTimeoutHandler和WriteTimeoutHandler可完美满足此需求。其中IdleSt

Netty源码解析6-ChannelHandler实例之LoggingHandler

LoggingHandler 日志处理器LoggingHandler是使用Netty进行开发时的好帮手,它可以对入站\出站事件进行日志记录,从而方便我们进行问题排查。首先看类签名: @Sharablepublic class LoggingHandler extends ChannelDuplexHandler 注解Sharable说明LoggingHandler没有状态相关变量,