ThreadLocal

ThreadLocal结构在这里插入图片描述

每个Thread都有自己专属的ThreadLocalMap,其中包含了多个ThreadLocal和对应的value值。同时ThreadLocal又存在于多个Thread的ThreadLocalMap key中,每个ThreadLocalMap.get(threadLocal)获得的value值不一样,这样就实现了同一个ThreadLocal对应每个线程都有特殊的副本值。
threadLocal.set(value) 相当于 CurrentThread - > ThreadLocalMap -> put(threadLocal,value)
threadLocal.get() 相当于CurrentThread -> ThreadLocalMap -> get(threadLocal)
在这里插入图片描述

线程安全

值得注意的是,虽然ThreadLocal存储了各个Thread的副本值,但是并不见得是线程安全的,当value是int这种基本类型的变量时,自然多线程操作不会影响。但当value是同一个对象 时,多线程同时操作,自然会出现线程安全问题。

public class ThreadLocalUnsafe implements Runnable {
    private static Number num =  new Number(0);

    private static ThreadLocal<Number> tl = new ThreadLocal<Number>(){
        @Override
        protected Number initialValue() {
            return new Number(0);
        }
    };

    public static void main(String[] args) {
        Thread[] arr = new Thread[5];
        for(int i=0;i<5;i++){
            Thread t = new Thread(new ThreadLocalUnsafe());
            arr[i] = t;
        }
        System.out.println("==============");
        Arrays.stream(arr).forEach(a->{a.start();});
    }

    @Override
    public void run() {
        num.setNum(num.getNum()+1);
        tl.set(num);
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+":"+ num.getNum());
    }


    private static class Number{
        int num;
        public Number(int num){
            this.num = num;
        }

        public void setNum(int num){
            this.num = num;
        }

        public int getNum(){
            return this.num;
        }

    }

}

因为多线程操作的是同一个static对象num,导致线程不安全。如果要避免这种情况发生,可以采用

  • num去除static
  • setNum操作new Number()

内存泄漏

内存泄漏指无法释放某些对象占用的空间。一般而言,JVM根据可达性分析判断堆对象是否有引用,如果没有引用,则在下次GC中进行回收。所以内存泄漏可以理解为,某些对象一直有引用,无法被回收。
引用又分为强、软、弱、虚四种,除了强引用存在时,堆对象一定无法被回收。但其他三种引用存在时,GC仍会遵循某种规律将对象进行回收。

强引用

下面这个例子设置-Xmx15m,最大堆内存15m。虽然每次都设置了abc=null,arr[]依然有指向new byte[]的强引用,新建的几个byte仍然可达,所以无法回收,内存泄漏最后大致内存溢出。

public class StrongReferer {
    public static void main(String[] args) throws InterruptedException {
        Object[] arr= new Object[5];
        for(int i=0;i<5;i++){
            Object abc = new byte[1024*1024*5];
            arr[i]=abc;
            System.out.println("创建第"+(i+1)+"个对象");
            abc=null;
            // arr[i]=null;
        }
    }
}

解决办法是每次循环结尾释放数组对byte[]的引用,arr[i]=null;
事实上Arraylist.clear()也是这么做的。

public void clear() {
        modCount++;
        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;
        size = 0;
    }
弱引用

弱引用即使存在,也会在下次GC的时候回收对象。这里依然设置-Xmx15m,往数组里插入弱引用类型的ref变量,arr有指向ref堆对象的弱引用,即便可达,仍会在下次GC的时候被回收,程序不会内存溢出。
弱引用有效避免了内存泄漏的发生!

public class WeakReferer {
    public static void main(String[] args) throws InterruptedException {
        Object[] arr= new Object[5];
        for(int i=0;i<5;i++){
            Object str=new byte[1024*1024*5];
            WeakReference<Object> ref = new WeakReference<Object>(str);
            arr[i] =ref;
            System.out.println("创建第"+(i+1)+"个对象");
        }
        System.out.println(arr);
    }
}

当程序创建第一个对象时,referent指向堆对象
在这里插入图片描述

但当程序运行到“创建第3个对象时”,发现arr[0]的referent已经为null了。
在这里插入图片描述

ThreadLocalMap中的弱引用

ThreadLocalMap中的entry中key,使用的是弱引用,好处是当指向ThreadLocal的强引用失效后,key指向的ThreadLocal弱引用会在下次GC时将key设置成null,然后ThreadLocalMap会在set(),get(),remove()时调用方法将该entry拿掉。这样就一定程度上避免了内存泄漏。
在这里插入图片描述
话不多说还是上例子,这里我们设置-Xmx30m,运行程序后发现不会溢出。但如果把arr[i]=tl的注释去掉,有了arr对tl堆对象的强引用,就无法回收内存,最终导致程序内存溢出。

public class ThreadLocalOOM2 extends Thread {
    public static void main(String[] args) {
        Object[] arr = new Object[8];
        for(int i=0;i<8;i++){
            System.out.println("set第"+(i+1)+"个对象");
            ThreadLocal<byte[]> tl = new ThreadLocal<>();
            tl.set(new byte[1024*1024*5]);
            // arr[i]=tl;
            System.gc();
        }
    }
}

那么为什么程序能GC回收ThreadLocal呢,自然要打断点进去一探究竟。
set第1个对象后,发现currentThread的ThreadLocalMap创建了新的Entry
在这里插入图片描述
在set第3个对象前,经过了System.gc(),再次观察ThreadLocalMap中的entry,发现原本指向第一个对象的ThreadLocal变为null了。
在这里插入图片描述
指向ThreadLocal堆对象的有两种引用

  • ThreadLocal tl = new ThreadLocal() ,这是个强引用,但循环后并没有用到tl,导致引用失效,可以被回收
  • ThreadLocalMap 中的tab[15] 对应的entry是 {threadLocal : new byte[]},但这个key对堆对象是个弱引用,也可以被回收
    所以threadLocal这个堆对象被回收了,最终tab[15]的key变成了null。

但这样就完了吗?value还存在呀!
只有把整个entry置为空,才能避免内存泄漏。幸运的是,threadLocalMap中set(),get(),remove()方法都会清除掉tab中key=null的entry。
(e.get() 是调用Reference.get()方法,获取referent即ThreadLocal)
在这里插入图片描述
至此,原先的tab[15]就被清除掉了,new byte[]没有任何引用也将被回收。
在这里插入图片描述

  • 有个比较有趣的地方,代码中特地加了System.gc(),如果不加的话,用run方法运行程序,会报内存溢出。
    在这里插入图片描述
  • 但用Debug运行,又不会报错
    在这里插入图片描述
  • 这应该只能理解成Run和Debug在gc上可能有些区别,Debug有更多的Stop the World时间进行回收

为何使用弱引用

引用:https://www.jianshu.com/p/a1cd61fa22da
最终目的是要清除entry,回收value,防止内存泄漏。倒过来推,需要把key置为null,才会回收entry。key在什么情况下会被回收呢,只有在ThreadLocalMap对其是弱引用时,才会在没有强引用存在的情况下进行回收。
在这里插入图片描述

  • key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
  • key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key的value就会导致内存泄漏,而不是因为弱引用。

该资源内项目源码是个人的课程设计、毕业设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。 该资源内项目源码是个人的课程设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值