-
ThreadLocal内存泄漏问题一 -
ThreadLocal内存泄漏问题二 -
内存泄漏的解决
TransmittableThreadLocal
前言
阿里巴巴java编程规范里有一条强制如下
1.ThreadLocal 设计
ThreadLocal 的目的是让不同的线程有不同的变量 V,如果不看源码,我们最直接的想法就是维护一个map,其中key是线程,value是每个线程的变量。这样每次通过get对应线程就可以拿到对应的变量。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
public class ThreadLocal<T> {
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
1.1 ThreadLocal内存泄漏问题一
记得我去年从平安离职时面试阿里被问到了一个问题。普通内部类和静态内部类会有内存泄漏问题吗。当时这个问题问的比较突然,一时没有反应过来,事后百度一下,查到答案总结如下
我们创建内部类需要先创建外部类
Outerclass out = new Outerclass();
Outerclass.innerclass in = out.new innerclass();
再回到threadLocal的设计,如果我们将threadLocalMap的static去掉,结合上面的分析是不是容易引起内存溢出的问题。因为我们无法确定threadLocal里会存的数据量有多大,这样风险很大。如果一直得不到回收,势必会有内存泄漏风险。
1.2 ThreadLocal内存泄漏问题二
我们再看一眼ThreadLocalMap类
ppublic class ThreadLocal<T> {
static class ThreadLocalMap {
private Entry[] table;
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}Thread持有ThreadLocalMap,而且ThreadLocalMap里对ThreadLocal的引用还是弱引用(WeakReference),如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,这个时候只要thead线程退出,value的强引用链条就会断掉。那么ThreadLocalMap也会被回收。所以只要thread线程退出,那么ThreadLocalMap就会被回收。如果不理解这句话,我们可以通过垃圾回收机制的可达性理论分析。 1.2.1 可达性分析
我们改造一下上面的图做参考
此时是thread正好符合第一条是栈中引用的对象,是可以作为GC Root 对象,当thread被回收,那么threadLocalMap到root节点间没有引用,同样也会被回收。
那么问题来了,当我们使用线程池时,那么线程的生命周期基本上和程序同生共死了。这就意味着 Thread 持有的 ThreadLocalMap 一直都不会被回收,再加上ThreadLocalMap 中的 Entry 对 ThreadLocal 是弱引用(WeakReference),所以只要 ThreadLocal 结束了自己的生命周期是可以被回收掉的。但是 Entry 中的 Value 却是被 Entry 强引用的,所以即便 Value 的生命周期结束了,Value 也是无法被回收的,从而导致内存泄露。 1.2.2 强弱引用讨论
这里再补充一个知识点WeakReference
可能有的人会问,如果把这个强引用链打断,就不会有这个问题发生。
然后参考key的设计,将value也继承weakReference,那么当垃圾回收时,如果threadLocal对外没有强引用,那么会被回收,即entry里key是null ,我们的value一般存的都是变量,threadLocal的设计就是为了保存变量,那么该变量可能在某次方法结束后没有强引用就被回收了,下面再去取就拿不到了。那threadLocal就失去了它的作用。
假如我们的key 即threadLocal使用强用会怎么样?
当threadLocalMap的key为强引用,回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,同样也会导致Entry内存泄漏。 1.3 内存泄漏的解决
那在线程池中,我们该如何正确使用 ThreadLocal 呢?其实很简单,既然 JVM 不能做到自动释放对 Value 的强引用,那我们手动释放就可以了。如何能做到手动释放呢?想必我们在代码中应该是看到过很多了。
alibaba开发指南
2.InheritableThreadLocal
我们继续看threadLocal的源码,前面提到threadLocalMap被thread引用,在默认情况下,每个线程中的这个变量都为null。那该map什么时候初始化的呢? 通过看源码我们知道,当线程第一次调用ThreadLocal的set或者get方法的时候才会创建他们。我们以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();
}
❶.getMap(t) 就是拿到thread的threadLocalMap.
前面提到ThreadLocal仅仅是一个代理工具类。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
❷setInitialValue() ,第一次调用时,此时threadLocalMap还是空,需要初始化
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
//❶
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
//❷
createMap(t, value);
return value;
}
❶处又从当前线程中获取了一次threadLocalMap,有点double check的味道,继续往下跟,如果还没有拿到map,则❷createMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
可以看到它不支持子线程。因为map是绑定在currentThread中的。子线程和父线程并不是一个Thread,所以产生了ThreadLocal的进化版本InheritableThreadLocal
其实thread不仅只引用了threadLocalMap
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
InheritableThreadLocal就是支持子线程的ThreadLocal
2.1 InheritableThreadLocal 原理
首先看源码,比较简单,全部贴出
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
/**
* 这个方法使用在线程Thread的构造函数里面ThreadLocal.createInheritedMap(),
* 基于父线程InheritableThreadLocal的属性创建子线程的InheritableThreadLocal属性,
* 它的返回值决定了拷贝父线程的属性时候传入子线程的值
*/
protected T childValue(T parentValue) {
return parentValue;
}
/**
* 覆盖获取线程实例中的绑定的ThreadLocalMap为Thread#inheritableThreadLocals
* 这个方法其实是覆盖了ThreadLocal中对应的方法,应该加@Override注解
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
/**
* 覆盖创建ThreadLocalMap的逻辑,赋值到线程实例中的inheritableThreadLocals
* 而不是threadLocals,这个方法其实是覆盖了ThreadLocal中对应的方法
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}其中childValue很明显就是父子进程有关,我们倒推分析,在哪里调用
private ThreadLocalMap(ThreadLocalMap parentMap) {
//❶
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
//❷
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}从❶❷中可以很明显的看到,在创建子threadLocalMap时会从父线程中拷贝.
我们接着倒推
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}继续往上跟
#Thread.class
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
this.name = name;
this.group = g;
// 子线程会继承父线程的守护属性
this.daemon = parent.isDaemon();
//子线程继承父线程的优先级属性
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
// 当父线程的 inheritableThreadLocals 的值不为空时
// 会把 inheritableThreadLocals 里面的值全部传递给子线程
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
// 线程 id 自增
tid = nextThreadID();
}
接着见证答案的时候到了
#Thread.class
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
也就是说InheritableThreadLocal支持子线程访问在父线程中设置的线程上下文环境的实现原理是在创建子线程时将父线程中的本地变量值复制到子线程,即复制的时机为创建子线程时。
但我们提到并发、多线程就离不开线程池的使用,因为线程池能够复用线程,减少线程的频繁创建与销毁,如果使用InheritableThreadLocal,那么线程池中的线程拷贝的数据来自于第一个提交任务的外部线程,即后面的外部线程向线程池中提交任务时,子线程访问的本地变量都来源于第一个外部线程,造成线程本地变量混乱
3.TransmittableThreadLocal
TransmittableThreadLocal简称(TTL)是阿里开源的一款支持线程池的ThreadLocal组件,专门用在使用线程池等会池化复用线程的执行组件情况下传递ThreadLocal值的情况。
3.1 典型场景例子(来源于官网)
1.分布式跟踪系统 或 全链路压测(即链路打标)
2.日志收集记录系统上下文
3.Session级Cache
4.应用容器或上层框架跨应用代码给下层SDK传递信息3.2 原理
主要是修饰了线程池
直接看官网示例代码
ExecutorService executorService = ...
// 额外的处理,生成修饰了的对象executorService
executorService = TtlExecutors.getTtlExecutorService(executorService);
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// 在父线程中设置
context.set("value-set-in-parent");
Runnable task = new RunnableTask();
Callable call = new CallableTask();
executorService.submit(task);
executorService.submit(call);
// =====================================================
// Task或是Call中可以读取,值是"value-set-in-parent"
String value = context.get();
TtlExecutors.getTtlExecutorService 到底做了什么,我们接着跟
@Nullable
public static ExecutorService getTtlExecutorService(@Nullable ExecutorService executorService) {
if (TtlAgent.isTtlAgentLoaded() || executorService == null || executorService instanceof TtlEnhanced) {
return executorService;
}
return new ExecutorServiceTtlWrapper(executorService, true);
}
可以看到它对ExecutorService做了包装,然后充血了submit方法
@NonNull
@Override
public Future<?> submit(@NonNull Runnable task) {
return executorService.submit(TtlRunnable.get(task, false, idempotent));
}
重点就是TtlRunnable类了,它实现了Runable方法,并且重写了run方法
public final class TtlRunnable implements Runnable, TtlWrapper<Runnable>, TtlEnhanced, TtlAttachments {
private final AtomicReference<Object> capturedRef;
private final Runnable runnable;
private final boolean releaseTtlValueReferenceAfterRun;
private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
this.capturedRef = new AtomicReference<Object>(capture());
this.runnable = runnable;
this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
}
/**
* wrap method {@link Runnable#run()}.
*/
@Override
public void run() {
final Object captured = capturedRef.get();
if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
throw new IllegalStateException("TTL value reference is released after run!");
}
final Object backup = replay(captured);
try {
runnable.run();
} finally {
restore(backup);
}
}
这个代码就是流程的核心了,就是在run方法之前复制了父线程的ThreadLocal变量。当线程执行时,调用 TtlRunnable run 方法,TtlRunnable 会从 AtomicReference 中获取出调用线程中所有的上下文,并把上下文给 TransmittableThreadLocal.Transmitter.replay 方法把上下文复制到当前线程。并把上下文备份。
当线程执行完,调用 TransmittableThreadLocal.Transmitter.restore 并把备份的上下文传入,恢复备份的上下文,把后面新增的上下文删除,并重新把上下文复制到当前线程。
因为你的分享、点赞、在看
我足足的精气神儿!
关注我的你,是最香哒!
原文始发于微信公众号(小李的源码图):从threadlocal到TransmittableThreadLocal
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/145503.html