随笔 - 171  文章 - 0  评论 - 0  阅读 - 62466

ThreadLocal

 
运行时,会在栈中产生两个引用,指向堆中相应的对象。
可以看到,ThreadLocalMap使用ThreadLocal的弱引用作为key,这样一来,当ThreadLocal ref和ThreadLocal之间的强引用断开 时候,即ThreadLocal ref被置为null,下一次GC时,threadLocal对象势必会被回收,这样,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,比如使用线程池,线程使用完成之后会被放回线程池中,不会被销毁,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。所以当前线程在用完threadlocal后,必须remove。

为什么要使用弱引用?

使用弱引用,是为了更好地对ThreadLocal对象进行回收。如果使用强引用,当ThreadLocal ref = null的时候,意味着ThreadLocal对象已经没用了,ThreadLocal对象应该被回收,但由于Entry中还存着这对ThreadLocal对象的强引用,导致ThreadLocal对象不能回收,可能会发生内存泄漏。

额外保证

对于线程池来说,大部分线程会一直存在在系统的整个生命周期内,那样的话,就会造成value对象出现泄漏的可能。处理的方法是,在ThreadLocalMap进行set(),get(),remove()的时候,都会进行清理:
以getEntry()为例:
复制代码
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        //如果找到key,直接返回
        return e;
    else
        //如果找不到,就会尝试清理,如果你总是访问存在的key,那么这个清理永远不会进来
        return getEntryAfterMiss(key, i, e);
}
复制代码

getEntryAfterMiss()

复制代码
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        // 整个e是entry ,也就是一个弱引用
        ThreadLocal<?> k = e.get();
        //如果找到了,就返回
        if (k == key)
            return e;
        if (k == null)
            //如果key为null,说明弱引用已经被回收了
            //那么就要在这里回收里面的value了
            expungeStaleEntry(i);
        else
            //如果key不是要找的那个,那说明有hash冲突,这里是处理冲突,找下一个entry
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}
复制代码

真正用来回收value的是expungeStaleEntry()方法,在remove()和set()方法中,都会直接或者间接调用到这个方法进行value的清理:

从这里可以看到,ThreadLocal为了避免内存泄露,也算是花了一番大心思。不仅使用了弱引用维护key,还会在每个操作上检查key是否被回收,进而再回收value。

但是从中也可以看到,ThreadLocal并不能100%保证不发生内存泄漏。

比如,很不幸的,你的get()方法总是访问固定几个一直存在的ThreadLocal,那么清理动作就不会执行,如果你没有机会调用set()和remove(),那么这个内存泄漏依然会发生。

因此,一个良好的习惯依然是:当你不需要这个ThreadLocal变量时,主动调用remove(),这样对整个系统是有好处的

ThreadLocalMap中的Hash冲突处理

它使用的是简单的线性探测法,如果发生了元素冲突,那么就使用下一个槽位存放。

所谓线性探测,就是根据初始key的hashcode值,确定元素在table数组中的位置,如果发现这个位置上,已经有其他key值的元素被占用,则利用固定的算法,寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

父子线程传递

使用InheritableThreadLocal。

复制代码
public static void main(String[] args) {
    ThreadLocal threadLocal = new ThreadLocal();
    //改为InheritableThreadLocal threadLocal = new InheritableThreadLocal();
    IntStream.range(0,10).forEach(i -> {
        //每个线程的序列号,希望在子线程中能够拿到
        threadLocal.set(i);
        //这里来了一个子线程,我们希望可以访问上面的threadLocal
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
        }).start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}
复制代码
  1. 变量的传递是发生在线程创建的时候,如果不是新建线程,而是用了线程池里的线程,就不灵了
  2. 变量的赋值就是从主线程的map复制到子线程,它们的value是同一个对象,如果这个对象本身不是线程安全的,那么就会有线程安全问题

线程池传递参数

在项目中很少会自己启线程去做逻辑处理,都是初始化线程池使用,那么InheritableThreadLocal就不管用了,需要用到TransmittableThreadLocal。
我们内部对线程池使用TtlExecutors.getTtlExecutor会进行wrap,通过warp后的ExecutorService提交任务,提交任务会对Runnable进行包装。
  1. 装饰Runnable,将主线程的TTL传入到TtlRunnable的构造方法中
  2. 将子线程的TTL的值进行备份,将主线程的TTL设置到子线程中(value是对象引用,可能存在线程安全问题);
  3. 执行子线程逻辑
  4. 删除子线程新增的TTL,将备份还原重新设置到子线程的TTL中
复制代码
@Override
public void run() {
    /**
     * capturedRef是主线程传递下来的ThreadLocal的值。
     */
    Object captured = capturedRef.get();
    if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
        throw new IllegalStateException("TTL value reference is released after run!");
    }
    /**
     * 1.  backup(备份)是子线程已经存在的ThreadLocal变量;
     * 2. 将captured的ThreadLocal值在子线程中set进去;
     */
    Object backup = replay(captured);
    try {
        /**
         * 待执行的线程方法;
         */
        runnable.run();
    } finally {
        /**
         *  在子线程任务中,ThreadLocal可能发生变化,该步骤的目的是
         *  回滚{@code runnable.run()}进入前的ThreadLocal的线程
         */
        restore(backup);
    }
}
 /**
 * 将快照重做到执行线程
 * @param captured 快照
 */
public static Object replay(Object captured) {
    // 获取父线程ThreadLocal快照
    final Snapshot capturedSnapshot = (Snapshot) captured;
    return new Snapshot(replayTtlValues(capturedSnapshot.ttl2Value), replayThreadLocalValues(capturedSnapshot.threadLocal2Value));
}

/*****************************************************
 * 重放TransmittableThreadLocal,并保存执行线程的原值
 ****************************************************/
private static WeakHashMap<TransmittableThreadLocalCode<Object>, Object> replayTtlValues(WeakHashMap<TransmittableThreadLocalCode<Object>, Object> captured) {
    WeakHashMap<TransmittableThreadLocalCode<Object>, Object> backup = new WeakHashMap<TransmittableThreadLocalCode<Object>, Object>();

    for (final Iterator<TransmittableThreadLocalCode<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
        TransmittableThreadLocalCode<Object> threadLocal = iterator.next();

        // backup
        // 遍历 holder,从 父线程继承过来的,或者之前注册进来的
        backup.put(threadLocal, threadLocal.get());

        // clear the TTL values that is not in captured
        // avoid the extra TTL values after replay when run task
        // 清除本次没有传递过来的 ThreadLocal,和对应值
        //  -- 第一点:可能会有因为 InheritableThreadLocal 而传递并保留的值
        //  -- 第二点:保证主线程set过的ThreadLocal不被传递过来。明确其传递是由业务代码控制,就是明确 set 过值的
        if (!captured.containsKey(threadLocal)) {
            iterator.remove();
            threadLocal.superRemove();
        }
    }

    // set TTL values to captured
    // 将 map 中的值,设置到快照
    // 内部调用了 beforeExecute 和 afterExecute 方法。默认不做任何处理
    setTtlValuesTo(captured);

    // call beforeExecute callback
    // TransmittableThreadLocal 的回调方法,在任务执行前执行
    doExecuteCallback(true);

    return backup;
}

private static WeakHashMap<ThreadLocal<Object>, Object> replayThreadLocalValues( WeakHashMap<ThreadLocal<Object>, Object> captured) {
    final WeakHashMap<ThreadLocal<Object>, Object> backup = new WeakHashMap<ThreadLocal<Object>, Object>();

    for (Map.Entry<ThreadLocal<Object>, Object> entry : captured.entrySet()) {
        final ThreadLocal<Object> threadLocal = entry.getKey();
        backup.put(threadLocal, threadLocal.get());

        final Object value = entry.getValue();
        // 如果值是标记已删除,则清除
        if (value == threadLocalClearMark) threadLocal.remove();
        else threadLocal.set(value);
    }

    return backup;
}
/*********************************************
 * 恢复备份的原快照
 *********************************************/
public static void restore( Object backup) {
    // 将之前保存的TTL和threadLocal原来的数据覆盖回去
    final Snapshot backupSnapshot = (Snapshot) backup;
    restoreTtlValues(backupSnapshot.ttl2Value);
    restoreThreadLocalValues(backupSnapshot.threadLocal2Value);
}

private static void restoreTtlValues( WeakHashMap<TransmittableThreadLocalCode<Object>, Object> backup) {
    // call afterExecute callback
    // 调用执行完后回调接口
    doExecuteCallback(false);

    // 移除子线程新增的TTL
    for (final Iterator<TransmittableThreadLocalCode<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
        TransmittableThreadLocalCode<Object> threadLocal = iterator.next();
        // 恢复快照时,清除本次传递注册进来,但是原先不存在的 TransmittableThreadLocal
        // 移除掉所有不在备份里面的TTL数据,应该是为了避免内存泄漏吧
        // clear the TTL values that is not in backup
        // avoid the extra TTL values after restore
        if (!backup.containsKey(threadLocal)) {
            iterator.remove();
            threadLocal.superRemove();
        }
    }

    // 重置为原来的数据(就是恢复回备份前的值)
    // restore TTL values
    setTtlValuesTo(backup);
}

private static void setTtlValuesTo( WeakHashMap<TransmittableThreadLocalCode<Object>, Object> ttlValues) {
    for (Map.Entry<TransmittableThreadLocalCode<Object>, Object> entry : ttlValues.entrySet()) {
        TransmittableThreadLocalCode<Object> threadLocal = entry.getKey();
        // set 的同时,也就将 TransmittableThreadLocal 注册到当前线程的注册表了
        threadLocal.set(entry.getValue());
    }
}
复制代码

 

 
posted on   zhengbiyu  阅读(19)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

点击右上角即可分享
微信分享提示