重温 JAVA -- ThreadLocal 终

AlgoRoverPro
• 阅读 1609

ThreadLocal 是什么

作用

ThreadLocal 用于存储线程间的私有变量

数据结构

重温 JAVA -- ThreadLocal 终

内存泄露?

要解释这个问题之前,需要先看 JAVA 对象中的 强引用、软引用、弱引用、虚引用

对象的四种引用类型

  • 强引用
    new 或通过反射创建出来的对象被称为强引用,只要强引用还存在,就不会被垃圾回收
  • 软引用
    使用 SoftReference 修饰的对象被称为软引用,当内存不足时,软引用对象会被回收
  • 弱引用
    使用 WeakReference 修饰的对象被称为弱引用,当对象只有弱引用时,GC 时,该对象会被回收
  • 虚引用
    使用 PhantomReference 修饰的对象被称为虚引用,当对象被回收时会收到系统通知

WeakReference 案例介绍

public class WeakReferenceObj extends WeakReference<Object> {


    public WeakReferenceObj(Object referent) {
        super(referent);
    }

    public static void main(String[] args) {
        WeakReferenceObj weak = new WeakReferenceObj(new Object());
        int i = 0;
        while(true){
            if((weak.get()) != null){
                i++;
                System.out.println("Object is alive for "+i+" loops - "+weak);
            }else{
                System.out.println("Object has been collected.");
                break;
            }
        }
    }
}

当以上程序运行了一段时间后,WeakReference 指向的对象就会只因被弱引用引用,而将对象回收
重温 JAVA -- ThreadLocal 终

若将上诉代码改造为下面的代码

public class WeakReferenceObj extends WeakReference<Object> {

    public WeakReferenceObj(Object referent) {
        super(referent);
    }

    public static void main(String[] args) {
        Object o = new Object();
        WeakReferenceObj weak = new WeakReferenceObj(o);
        int i = 0;
        while(true){
            System.out.println(o);
            if((weak.get()) != null){
                i++;
                System.out.println("Object is alive for "+i+" loops - "+weak);
            }else{
                System.out.println("Object has been collected.");
                break;
            }
        }
    }
}

你会发现,不管运行多久,弱引用指向的对象都不会被回收。因为此时的 o 还被一个强引用指向。即 打印流

ThreadLocal 中的内存泄露

ThreadLocal 做为弱引用存在于 ThreadLocalMap key 中。因为是弱引用,当 ThreadLocal 只被弱引用指向时,在触发 GC 后,ThreadLocal 会被回收,即 ThreadLocalMap key 会为 nullvalueThreadLocalMap 强引用指向,导致 value 无法被回收。ThreadLocalMap 又是 Thread 的一个属性,因此除非 Thread 销毁,ThreadLocalMap 才会被释放,这样一来,Entry 不为 null ,key = null, value 又有值(占着茅坑不拉屎),ThreadLocalMap 如果没有有效的 清理 Entry 不为 null, key = null 的机制,那么就会因为 value 无法被回收,从而导致内存泄露。

ThreadLocal 清理机制

ThreadLocal 内存泄露的分析中,我们知道,如果 ThreadLocal 没有有效的清理机制,那么必然会导致内存泄露。那么接下来将介绍 ThreadLocal2 种清理机制,防止内存泄露

探测式清理

代码的逻辑在:ThreadLocalMap.expungeStaleEntry
key = null 的位置向前清理,然后遍历 ThreadLocalMap 直到 Entry != null。如果遇到 key = null 则将 Entry、value 设置为 null,如果 key != null, 则重新 hash 重新将该 Entry 放入 ThreadLocalMap 中。

ThreadLocalget(), set(T t),从何处开始清理不大一样,但是最终都是调用 expungeStaleEntry 方法,进行清理。

get()

清理点为:在从 x 下标 获取不到对应 keyvalue 时,会从 x 下标开始清理

set(T t)

清理点为:如果在设置值时,发现在 x 下标 key = null。则会从 x 往前查找 key = null,直到 Entry = null,如果查找到, x 会被赋予刚才元素的下标。最后再从 x 处开始清理。

启发式清理

代码的逻辑在:ThreadLocalMap.cleanSomeSlots

i 位置开始,直到当前 ThreadLocalMapEntry 个数 n >>> 1 != 0
如果 Entry != null,但 key = null, 会调用 expungeStaleEntry 进行清理。

如何预防

使用完毕后,调用 remove 方法,进行清理。

结论

get、set 方法在内部均会对过期 key 进行清理。但是为了以防万一,在使用完毕后,还需要手动调用 remove 方法进行清理

ThreadLocal Hash 算法

ThreadLocal 中有个属性 HASH_INCREMENT = 0x61c88647,它被称为 黄金分割数hash 增量为该数字,因此,产生的 hash 数值非常的均匀。

private static final int HASH_INCREMENT = 0x61c88647;

    public static void main(String[] args) {
        for (int i = 0; i < 16; i++) {
            int hash = HASH_INCREMENT * i + HASH_INCREMENT;
            System.out.print(hash & (16 - 1));
            System.out.print(",");
        }
    }

生成的结果如下:

7,14,5,12,3,10,1,8,15,6,13,4,11,2,9,0,

ThreadLocal Hash 冲突

ThreadLocal Hash 冲突使用的是 线性探测再散列的开放寻址法
所谓线性探测算法如下:从当前发生冲突的位置,往后查找,直到找到一个 null 的位置插入。

扩容

当进行 set 后,会执行 cleanSomeSlots 如果没有清理元素,且数组大小达到数组扩容阈值 thresholdlen * 2 / 3)则会进行探测式清理。如果清理完毕后,数组大小大于 treshold * 3 / 4 则进行扩容。

扩容时,数组变为原来的 2 倍,且将整个 ThreadLocalMapkey 重新 hash 放入 table

灵魂拷问,为什么 ThreadLocalMap key 是弱引用?

key 是强引用

ThreadLocalMap 的生命周期与 Thread 一致。如果 Thread 存活太久,添加了非常多的 ThreadLocal。此时若在代码中将 ThreadLocal 设置为 null,理应被回收。但是,因为 ThreadLocalMap 还存在 ThreadLocal 的强引用,而导致无法被回收,从而导致内存泄露。并且在代码里面,很难判断 ThreadLocal 在别的地方还有没有引用。

key 是弱引用

ThreadLocal 是弱引用,代码中被设置为 null 后,因为只存在弱引用,所以,在 GC 后会被正常回收。但是 key = null 也会存在 value 内存泄露。虽然 value 会存在内存泄露,但是可以通过判断 key = null 来判断,ThreadLocal 已没有其他引用。

结论

个人认为最核心的原因是:ThreadLocalMap 的生命周期与 Thread 一致。太过难于判断ThreadLocal 只在 ThreadLocalMap 中有引用。因此设置为弱引用,让 GC 回收 ThreadLocal 后,用 null 来判断

参考

面试官:听说你看过ThreadLocal源码?我来瞅瞅?

点赞
收藏
评论区
推荐文章
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
3年前
java ThreadLocal
ThreadLocal是什么定义:提供线程局部变量;一个线程局部变量在多个线程中,分别有独立的值(副本)特点:简单(开箱即用)、快速(无额外开销)、安全(线程安全)场景:多线程场景(资源持有、线程一致性、并发计算、线程安全等场景)ThreadLocal基本API 构
Wesley13 Wesley13
3年前
Java ThreadLocal的内存泄漏问题
ThreadLocal提供了线程独有的局部变量,可以在整个线程存活的过程中随时取用,极大地方便了一些逻辑的实现。常见的ThreadLocal用法有:\存储单个线程上下文信息。比如存储id等;\使变量线程安全。变量既然成为了每个线程内部的局部变量,自然就不会存在并发问题了;\减少参数传递。比如做一个trace工具,能够输出工程从开始到结
Wesley13 Wesley13
3年前
Java多线程与并发之ThreadLocal原理解析
1\.ThreadLocal是什么?使用场景ThreadLocal简介ThreadLocal是线程本地变量,可以为多线程的并发问题提供一种解决方式,当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,
Wesley13 Wesley13
3年前
Java并发编程的艺术笔记(四)——ThreadLocal的使用
ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。目的就是为了让线程能够有自己的变量可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值
Wesley13 Wesley13
3年前
Java多线程与并发之ThreadLocal
1\.ThreadLocal是什么?使用场景ThreadLocal简介ThreadLocal是线程本地变量,可以为多线程的并发问题提供一种解决方式,当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,
Wesley13 Wesley13
3年前
JAVA基础系列:ThreadLocal
1. 思路1.什么是ThreadLocal?ThreadLocal类顾名思义可以理解为线程本地变量。也就是说如果定义了一个ThreadLocal,每个线程往这个ThreadLocal中读写是线程隔离,互相之间不会影响的。它提供了一种将可变数据通过每个线程有自己的独立副本从而实现线程封闭的机制。2.它大致的实现
Easter79 Easter79
3年前
ThreadLocal 原理和使用场景分析
ThreadLocal不知道大家有没有用过,但至少听说过,今天主要记录一下ThreadLocal的原理和使用场景。使用场景直接定位到ThreadLocal的源码,可以看到源码注释中有很清楚的解释:它是线程的局部变量,这些变量只能在这个线程内被读写,在其他线程内是无法访问的。ThreadLocal定义的通常是
Easter79 Easter79
3年前
ThreadLocal 详解
概念ThreadLocal用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变量。也就是说ThreadLocal可以为每个线程创建一个【单独的变量副本】,相当于线程的privatestatic类型变量。使用示例publicclassThreadLocalTest{
Easter79 Easter79
3年前
ThreadLocal设计模式
ThreadLocal设计模式使用的也很频繁,会经常在各大框架找到它们的踪影,如struts2以及最近正在看的SpringAOP等。ThreadLocal设计模式也有很多误解,我的理解是(1)ThreadLocal所操作的数据是线程间不共享的。它不是用来解决多个线程竞争同一资源的多线程问题。(2)ThreadLocal所操作的数据主要
Easter79 Easter79
3年前
ThreadLocal的深入理解及应用
是什么?ThreadLocal很容易让人望文生义,想当然地认为是一个“本地线程”。其实,ThreadLocal并不是一个Thread,它类似(Map),用来存储当前运行线程及对应的变量。在WEB应用中每次Http请求,都相当于从线程池取一个空闲线程对请求的方法作处理。此时当前线程的所有方法中Thread.currentThread