ThreadLocal全解析以及Java的强软弱虚引用

ThreadLocal全解析

最近在面试一家还算二三线的略知名厂时,被问到了ThreadLocal,虽然大致的使用方式是回答出来了,但面试官问到使用ThreadLocal需要注意什么,以及它为什么会导致内存泄漏的问题时,就答不好了。
所以写篇文章记录一下。
ThreadLocal本身并不复杂,但面试一旦被问到,拖个5-10分钟还是没问题的,感觉也算是一个有“性价比”的知识点。

Java的四种引用(强软弱虚)

首先需要一点前置知识,Java中实际上有4种引用。

  1. 强引用
    在之前的学习中了解到gc会收集那些没有引用指向的对象,这里的引用实际上指的是默认的强引用,这里写一个类重写finalize()方法(在被回收时被调用,可以用来观察对象是否被回收)
public class MyObject {
    @Override
    protected void finalize() throws Throwable {
        System.out.println("finalize");
        super.finalize();
    }
}

没有强引用指向的对象会被gc回收掉,反过来说就是有强引用指向的对象一定不会被回收,这就是”强“的意思。

public class NormalRef {
    public static void main(String[] args) {
        MyObject myObject = new MyObject();
        myObject = null;
        System.gc();
    }
}

运行这段代码,可以看到,将强引用赋null后,myObject没有强引用指向,因此在调用gc后被回收,打印”finalize“。
在这里插入图片描述
2.软引用
看代码

public class SoftRef {
    public static void main(String[] args) throws InterruptedException {
        // 一个软引用指向一个10M的数组
        SoftReference<byte[]> softReference = new SoftReference<>(new byte[1024 * 1024 * 10]);
        System.gc();
        System.out.println(softReference.get()); // 可以看到 软引用没有被回收

        Thread.sleep(2000);

        // new 一个12m的对象,此时内存不够,发生gc
        byte[] bytes = new byte[1024 * 1024 * 12];

        System.out.println(softReference.get());// 内存不够用时 软引用被回收
    }
}

执行这段程序时,需要设置jvm的一些参数:最大堆20m,打印gc信息
在这里插入图片描述

可以看到,第一次gc时,有软引用指向数组,不会被回收;第二次内存不够时,软引用被回收。
在这里插入图片描述

软引用常被使用于做缓存,比如读取一个大图片100m,当我内存一直够用时,那就一直放在内存中,当内存不够时,就把这块内存回收。

  1. 弱引用
    看代码
public class WeakRef {
    public static void main(String[] args) {
        WeakReference<MyObject> weakReference = new WeakReference<>(new MyObject());
        System.gc();
        System.out.println(weakReference.get());
    }
}

弱引用很简单,只要遭遇gc,就会被回收。
在这里插入图片描述
那弱引用一gc就被回收,还有什么作用呢?实际上常用于容器中(先记住结论)
引申:WeakHashMap

  1. 虚引用
    虚引用需要传入一个引用的对象+一个引用队列,无法通过当前虚引用获得对象,但在当前对象被回收时,会将引用添加到引用队列中,相当于只起了一个记录的作用。
public class PhantomRef {
    // 定义一个list,等下不断的往list中扔值触发gc
    static List<Object> list = new LinkedList<>();

    // 定义一个引用队列(阻塞式的)
    static ReferenceQueue<MyObject> queue = new ReferenceQueue<>();

    public static void main(String[] args) {
        // 虚引用指向MyObject,同时传入一个引用队列
        PhantomReference<MyObject> myObjectPhantomReference = new PhantomReference<MyObject>(new MyObject(),queue);

        new Thread(()->{
            while(true){
                // 不断往list里家数据,模拟内存满了gc
                list.add(new byte[1024*1024]);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 虚引用永远获取不到值
                System.out.println(myObjectPhantomReference.get());
            }
        }).start();

        // 另一个线程不断的从队列里拿数据
        // 虚引用指向的对象被回收后,会自动把这个引用加入队列
        new Thread(()->{
            while (true){
                Reference<? extends MyObject> ref = queue.poll();
                if(ref != null){
                    System.out.println(ref);
                    System.out.println("对象被回收了");
                }
            }
        }).start();
    }
}

ThreadLocal入门理解

ThreadLocal,即线程本地变量。是一个以ThreadLocal变量为键,任意对象为值的数据结构。有set、get方法。可以理解为线程独有的一块变量。
应用场景:方法计时,代码如下

public class Profiler {
    // 线程本地变量,保存的是时间的值
    public static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<>();

    public static final void begin(){
        TIME_THREADLOCAL.set(System.currentTimeMillis());
    }

    public static final long end(){
        return System.currentTimeMillis() - TIME_THREADLOCAL.get();
    }

    public static void main(String[] args) throws InterruptedException {
        Profiler.begin();
        TimeUnit.SECONDS.sleep(1);
        System.out.println(end());
    }
}

这是一个计时器的工具类。假设不用ThreadLocal会怎样?在并发场景下Profiler的静态时间变量可能会被2个线程同时调用begin()而导致其中一个线程的数据被覆盖。所以用了ThreadLocal对时间变量做包装,这样每一个线程的数据都是线程私有的,不会产生覆盖写这种并发问题
可以定义一个切面,对所有Controller层的方法进行拦截,joinPoint(AOP中的概念)获取方法名+在方法调用的前后调用上面工具类的Start和end,打印方法执行时间

ThreadLocal源码解析

set源码 先看set方法
在这里插入图片描述
点进getMap,发现实际获取的是Thread下的一个变量map,这也能解释为什么是线程私有的了。
在这里插入图片描述
总结一下set就是:

先获取当前线程
再通过线程获取线程私有的ThreadLocalMap,这个ThreadLocalMap是ThreadLocal的一个内部类
再往ThreadLocalMap里塞值,其中key又是当前ThreadLocal的引用,V是具体的值

相信这个会让人觉得绕来绕去的,特别是往Map里塞值为什么又要是ThreadLocal自己的引用会让人觉得懵逼,实际仔细捋捋就知道了:
ThreadLocalMap是Thread的一个字段,说明这个map是线程私有的,那也代表着一个线程只有一个map。此时如果有多个ThreadLocal变量,那在同一个线程中获取的就是同一个map,那要怎么获取不同ThreadLocal对应的值呢?就是把key设置成threadLocal!

get源码
直接点进get源码
在这里插入图片描述
总结一下get就是:

依然是获取当前线程的map
通过this的key获取对应Entry
再通过Entry获取Value
如果map没有设置值,则设置默认值并返回

看完set和get,好像很简单的亚子。ThreadLocal和4种引用又是什么关系?
别急,下面才是重头!
上文只讲了有一个线程私有的map,map里的key是threadLocal的引用,但map的结构是什么样子的?
ThreadLocalMap源码
在这里插入图片描述

从上面至少可以看出
ThreadLocalMap是ThreadLocal的一个内部类
ThreadLocalMap有一个Entry的内部类(Entry就是Map中的一个k、v键值对)

这个Entry很重要,可能看它的源码又会懵逼:Entry怎么继承了一个弱引用<ThreadLocal<?>>,且为什么字段只有一个value,而没有key呢(而HashMap的Entry(Node)就有key 也有 value)
解释:

  1. Entry继承了WeakReference,说明Entry也是一个弱引用,WeakReference中的泛型参数是ThreadLocal<?>,说明Entry是一个指向ThreadLocal的弱引用,实际上这个弱引用就是指向key的引用
  2. Entry中的value就是一个强引用,可以直接通过Entry获取value值
    用一个图来总结上文
    在这里插入图片描述

假设有一个ThreadLocal类型的变量tl,进行set操作
首先,是往当前线程的map里放值
这个Map的每一个键值对都是一个Entry,这个Entry的弱引用指向了一个key,Entry中有一个Value

那为什么Entry要用一个弱引用指向ThreadLocal?
原因:

现在假设Entry同普通Map一样,有一个Key字段。
假设有一个ThreadLocal局部变量tl,那这个tl应该在方法执行完毕后变为垃圾对象,局部变量tl指向ThreadLocal实例的引用消失
但由于ThreadLocalMap是线程私有的一个字段,而线程有时是不会回收的(比如线程池的核心线程),或是线程存活时间很长,这个map将一直存在
而map里的key指向了这个ThreadLocal的实例,因此ThreadLocal实例依然有强引用存在,无法被垃圾回收!
但实际上这个threadLocal实例在方法结束后就不会再用到了!应该被回收!这就是内存泄漏

所以用一个弱引用指向ThreadLocal实例,在强引用还存在时,不会被回收,强引用一旦消失,这个实例对象就会被回收。这样好像就完美了。
但,依然没有这么简单。假设现在threadLocal实例被回收了,此时Map中的Entry还是存在的,只是Entry中的key变为了null,但value还是一直存在!
若创建了很多个ThreadLocal实例,实际上就是创建了很多的Entry在map中,就算threadLocal被回收,但map中的entry和entry中的value不会被回收!依然会可能出现内存泄漏
因此,使用ThreadLocal注意的情况为:
在每一次使用完ThreadLocal后,都要手动调用一下ThreadLocal.remove()方法,将当前Entry 删除,防止内存泄漏

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值