JDK源码系列:ThreadLocal弱引用真的是过度设计吗?

JDK源码系列:ThreadLocal弱引用真的是过度设计吗?_第1张图片

在《码处高效:Java开发手册》这本书上详细描述了ThreadLocal的原理,也有过度设计的说法,

JDK源码系列:ThreadLocal弱引用真的是过度设计吗?_第2张图片

难道弱引用设计真的没必要吗?对此老吕要仔细分析分析,ThreadLocal到底该不该使用弱引用设计。

JDK作为构建Java应用生态的原始生产资料和工具,它的每一行代码相信都是经过深思熟虑的(也有考虑不周的,后续版本会标上废弃或者改进代码)。

回忆ThreadLocal原理

JDK源码系列:ThreadLocal弱引用真的是过度设计吗?_第3张图片

ThreadLocal本质是一个访问Thread上ThreadLocalMap对象的操作工具类;

为什么业务代码不能直接访问Thread获取ThreadLocalMap来操作呢?

1、因为类ThreadLocalMap是定义在ThreadLocal的一个内部类,访问权限是protected级别,这就是说你的应用代码是无法直接定义使用ThreadLocalMap类的。

2、实例变量 ThreadLocalMap在Thread中定义的访问权限也是protected级别,你的业务代码也是无法直接访问的。

所以你只能通过ThreadLocal去访问Thread上的ThreadLocalMap对象。

(有通过反射想法去访问Thread上ThreadLocalMap对象并操作Map的同学吗?可以去试一把)

在Entry对象中key是指向ThreadLocal对象的弱引用,Value是指向值对象的强引用;

所以ThreadLocal机制的核心技术是在ThreadLocalMap类上。

回忆ThreadLocalMap内存释放原理

前面文章中也讲清楚了 ThreadLocalMap内存释放原理和触发场景

最理想的情况是 主动调用 remove方法来释放不再使用的Map条目

在内存泄漏、哈希冲突、扩容等情况下 ThreadLocalMap会顺便清理失效的条目,释放内存,从而将有效条目放到更优的位置

强引用设计的后果

直接后果就是 1、ThreadLocal 对象内存泄漏;2、ThreadLocalMap的失效条目没有任何被回收的机会,直到线程结束;

内存泄漏的直接原因是  线程上下文在释放当前线程对应ThreadLocalMap对应Entry之前 释放了 ThreadLocal对象的强引用,导致业务代码不可能再访问到那个Entry,但是Entry内存又无法被GC,从而形成内存泄漏。

弱引用设计的好处

1、至少ThreadLocal对象可以被GC;

2、在线程结束之前在某些场景下 Entry有被回收的机会;

ThreadLocalMap与HashMap的对比

ThreadLocalMap也是一个map但没有实现Map接口,只是一个实现了主要接口(get\set\remove)的为ThreadLocal量身定制的Map.

key引用设计

能否回收失效条目

HashMap

强引用

不能,原因:不能判断哪些条目已失效

ThreadLocalMap

弱引用

能,原因:能通过判断key=null发现失效的条目,从而进行回收。弱引用的设计使得ThreadLocal对象有机会被GC,从而导致key=null

通过实验测试失效条目的回收

触发失效条目回收是有条件的,本实验只触发了部分场景的回收,只是为了说明它确实能回收失效条目,弱引用的设计是有很大意义的。

先说下影响自动回收的因素:

1、只有弱引用对象被GC后,才会产生失效条目(key=null),才有触发失效条目回收的可能;

2、弱引用对象被GC之前,ThreadLocalMap的回收机制根本无法判断出失效条目,这时和强引用是没有区别的;

3、在弱引用对象被GC后,产生失效条目,在下一次get、set、remove时都有机会触发回收,但是并不一定能触发;

4、在hash冲突出现的情况下,触发回收的机会会大大提升;

5、在没有set的情况下直接get会触发条目初始化操作,这时相当于一个set操作,也有机会触发回收;

6、在没有set的情况下直接remove不一定会触发回收,因为不一定发生hash冲突;

公共代码:

/**
 * @Project fighting-core
 * @Description 用来填充 value
 * @Author lvaolin
 * @Date 2023/4/1 上午11:43
 */
public class MyBigObject {
    private String bigString;
    public MyBigObject(String bigString){
        this.bigString = bigString;
    }
}


/**
 * @Project fighting-core
 * @Description 反射工具
 * @Author lvaolin
 * @Date 2023/4/2 下午6:40
 */
public class MyReflectUtil {


    /**
     * 打印当前线程上关联的ThreadLocalMap容量和条目数量(包括有效和无效条目)
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    public static void printThreadLocalMapSize() throws NoSuchFieldException, IllegalAccessException {
        Thread thread = Thread.currentThread();
        Field threadLocalsField = Thread.class.getDeclaredField("threadLocals");
        threadLocalsField.setAccessible(true);
        Object threadLocals = threadLocalsField.get(thread);


        Field tableField = threadLocals.getClass().getDeclaredField("table");
        tableField.setAccessible(true);
        Object entryArr = tableField.get(threadLocals);
        int length = Array.getLength(entryArr);//threadLocalMap  length


        Field sizeField = threadLocals.getClass().getDeclaredField("size");
        sizeField.setAccessible(true);
        Object size = sizeField.get(threadLocals);//threadLocalMap  size
        System.out.println("threadLocalMap:length->"+length+",size->"+size);
    }
}

场景验证1:弱引用对象被GC之前,是不会触发自动回收的

/**
 * @Project fighting-core
 * @Description threadLocalMap内存回收验证
 * @Author lvaolin
 * @Date 2023/4/1 上午11:41
 */
public class MyService1 {


    public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
        MyService1 myService = new MyService1();
        myService.m1();
    }


    public void m1() throws IOException, NoSuchFieldException, IllegalAccessException {
        System.out.println("map初始状态:");
        MyReflectUtil.printThreadLocalMapSize();
        for (int i = 1; i <= 100; i++) {
            ThreadLocal tl = new ThreadLocal();
            tl.set(new MyBigObject("set big object"+tl.hashCode()));
        }
        System.out.println("map存储100个条目后:");
        MyReflectUtil.printThreadLocalMapSize();
    }
}


map初始状态:
threadLocalMap:length->16,size->3
map存储100个条目后:
threadLocalMap:length->256,size->103

场景验证2:弱引用对象被GC之后,是会触发自动回收的

public class MyService2 {


    public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
        MyService2 myService = new MyService2();
        myService.m1();
    }


    public void m1() throws IOException, NoSuchFieldException, IllegalAccessException {
        System.out.println("map初始状态:");
        MyReflectUtil.printThreadLocalMapSize();
        for (int i = 1; i <= 100; i++) {
            ThreadLocal tl = new ThreadLocal();
            tl.set(new MyBigObject("set big object"+tl.hashCode()));
            //x越小,gc越频繁
            // threadlocal弱引用对象回收越及时,
            // threadlocalMap回收越及时(前面文章讲了什么情况下触发回收)
            int x = 1;
            if (i%x==0)System.gc();


        }
        System.out.println("map存储100个条目后:");
        MyReflectUtil.printThreadLocalMapSize();


    }
    
x=1情况下:
map初始状态:
threadLocalMap:length->16,size->3
map存储100个条目后:
threadLocalMap:length->16,size->5


x=20情况下:
map初始状态:
threadLocalMap:length->16,size->3
map存储100个条目后:
threadLocalMap:length->64,size->24   
     
x=100情况下:
map初始状态:
threadLocalMap:length->16,size->3
map存储100个条目后:
threadLocalMap:length->256,size->103

场景验证3:弱引用对象被GC之后,get操作触发回收

public class MyService3 {


    public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
        MyService3 myService = new MyService3();
        myService.m1();
    }


    public void m1() throws IOException, NoSuchFieldException, IllegalAccessException {
        System.out.println("map初始状态:");
        MyReflectUtil.printThreadLocalMapSize();
        for (int i = 1; i <= 100; i++) {
            ThreadLocal tl = new ThreadLocal();
            tl.set(new MyBigObject("set big object"+tl.hashCode()));
        }
        System.out.println("map存储100个条目后:");
        MyReflectUtil.printThreadLocalMapSize();


        System.gc();
        ThreadLocal tl = new ThreadLocal();
        //get时如果没有找到会进行条目初始化,默认key=threadLocal,value=null
        tl.get();
        //tl.set(new MyBigObject("set big object"+tl.hashCode()));
        //tl.remove();
        System.out.println("GC后进行get操作后:");
        MyReflectUtil.printThreadLocalMapSize();
    }
}


map初始状态:
threadLocalMap:length->16,size->3
map存储100个条目后:
threadLocalMap:length->256,size->103
GC后进行get操作后:
threadLocalMap:length->256,size->4

场景验证4:弱引用对象被GC之后,set操作触发回收

public class MyService4 {
    public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
        MyService4 myService = new MyService4();
        myService.m1();
    }
    public void m1() throws IOException, NoSuchFieldException, IllegalAccessException {
        System.out.println("map初始状态:");
        MyReflectUtil.printThreadLocalMapSize();
        for (int i = 1; i <= 100; i++) {
            ThreadLocal tl = new ThreadLocal();
            tl.set(new MyBigObject("set big object"+tl.hashCode()));
        }
        System.out.println("map存储100个条目后:");
        MyReflectUtil.printThreadLocalMapSize();


        System.gc();
        ThreadLocal tl = new ThreadLocal();
        //tl.get();
        tl.set(new MyBigObject("set big object"+tl.hashCode()));
        //tl.remove();
        System.out.println("GC后进行set操作后:");
        MyReflectUtil.printThreadLocalMapSize();
    }
}


map初始状态:
threadLocalMap:length->16,size->3
map存储100个条目后:
threadLocalMap:length->256,size->103
GC后进行set操作后:
threadLocalMap:length->256,size->4

场景验证5:弱引用对象被GC之后,remove操作

public class MyService5 {


    public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
        MyService5 myService = new MyService5();
        myService.m1();
    }


    public void m1() throws IOException, NoSuchFieldException, IllegalAccessException {
        System.out.println("map初始状态:");
        MyReflectUtil.printThreadLocalMapSize();
        for (int i = 1; i <= 100; i++) {
            ThreadLocal tl = new ThreadLocal();
            tl.set(new MyBigObject("set big object"+tl.hashCode()));
        }
        System.out.println("map存储100个条目后:");
        MyReflectUtil.printThreadLocalMapSize();


        System.gc();
        ThreadLocal tl = new ThreadLocal();
        //tl.get();
        //tl.set(new MyBigObject("set big object"+tl.hashCode()));
        tl.remove();
        System.out.println("GC后进行remove操作后:");
        MyReflectUtil.printThreadLocalMapSize();
    }
}


map初始状态:threadLocalMap:length->16,size->3
map存储100个条目后:threadLocalMap:length->256,size->103
GC后进行remove操作后:threadLocalMap:length->256,size->103

因为没有set就直接进行了remove,remove定位元素时也没有发生hash冲突,所以就没有触发任何回收场景;

总结

1、ThreadLocal机制的弱引用设计是有意义的,它能在正常操作中尽力自动回收泄漏的内存,避免hash扩容,降低内存占用;

2、如果没有弱引用设计,那么ThreadLocalMap将和HashMap一样不能自动回收任何内存;

3、ThreadLocalMap是专门为ThreadLocal机制定制的,只实现了基本get、set、remove操作,特色是使用了线性探测法来解决hash冲突问题,

同时map key 弱引用的设计为自动回收泄漏内存奠定了基础;

你可能感兴趣的:(公众号:,老吕架构,JDK,java,jdk)