聊聊ThreadlLocal和引用的那些事

1 背景

某一天在某一个群里面的某个群友忽然提出了一个问题:"threadlocal的key是弱引用,那么在threadlocal.get()的时候,发生GC以后,key是不是null?"屏幕前的你能够好好的想一想这个问题,在这里我先卖个关子,先讲讲Java中引用和ThreadLocal的那些事。java

2 Java中的引用

对于不少Java初学者来讲,会把引用和对象给搞混淆。下面有一段代码,数组

User zhangsan = new User("zhangsan", 24);

这里先提个问题zhangsan究竟是引用仍是对象呢?不少人会认为zhangsan是个对象,若是你也是这样认为的话那么再看一下下面一段代码缓存

User zhangsan;
zhangsan = new User("zhangsan", 24);

这段代码和开始的代码其实执行效果是一致的,这段代码的第一行User zhangsan,定义了zhangsan,那你认为zhangsan仍是对象吗?若是你还认为的话,那么这个对象应该是什么呢?的确,zhangsan其实只是一个引用,对JVM内存划分熟悉的同窗应该熟悉下面的图片:
聊聊ThreadlLocal和引用的那些事app

其实zhangsan是栈中分配的一个引用,而new User(“zhangsan”, 24)是在堆中分配的一个对象。而’='的做用是用来将引用指向堆中的对象的。就像你叫张三但张三是个名字而已并非一个实际的人,他只是指向的你。ide

咱们通常所说的引用其实都是代指的强引用,在JDK1.2以后引用不止这一种,通常来讲分为四种:强引用,软引用,弱引用,虚引用。而接下来我会一一介绍这四种引用。工具

2.1 强引用

上面咱们说过了 User zhangsan = new User(“zhangsan”, 24);这种就是强引用,有点相似C的指针。对强引用他的特色有下面几个:性能

1,强引用能够直接访问目标对象。flex

2,只要这个对象被强引用所关联,那么垃圾回收器都不会回收,那怕是抛出OOM异常。this

3,容易致使内存泄漏。.net

2.2 软引用

在Java中使用SoftReference帮助咱们定义软引用。其构造方法有两个:

public SoftReference(T referent);
public SoftReference(T referent, ReferenceQueue<? super T> q);

两个构造方法类似,第二个比第一个多了一个引用队列,在构造方法中的第一个参数就是咱们的实际被指向的对象,这里用新建一个SoftReference来替代咱们上面强引用的等号。 下面是构造软引用的例子:

softZhangsan = new SoftReference(new User("zhangsan", 24));

2.2.1软引用有什么用?

若是某个对象他只被软引用所指向,那么他将会在内存要溢出的时候被回收,也就是当咱们要出现OOM的时候,若是回收了一波内存还不够,这才抛出OOM,弱引用回收的时候若是设置了引用队列,那么这个软引用还会进一次引用队列,可是引用所指向的对象已经被回收。这里要和下面的弱引用区分开来,弱引用是只要有垃圾回收,那么他所指向的对象就会被回收。下面是一个代码例子:

public static void main(String[] args) {
        ReferenceQueue<User> referenceQueue = new ReferenceQueue();
        SoftReference softReference = new SoftReference(new User("zhangsan",24), referenceQueue);
        //手动触发GC
        System.gc();
        Thread.sleep(1000);
        System.out.println("手动触发GC:" + softReference.get());
        System.out.println("手动触发的队列:" + referenceQueue.poll());
        //经过堆内存不足触发GC
        makeHeapNotEnough();
        System.out.println("经过堆内存不足触发GC:" + softReference.get());
        System.out.println("经过堆内存不足触发GC:" + referenceQueue.poll());
    }

    private static void makeHeapNotEnough() {
        SoftReference softReference = new SoftReference(new byte[1024*1024*5]);
        byte[] bytes = new byte[1024*1024*5];
    }
    输出:
    手动触发GC:User{name='zhangsan', age=24}
    手动触发的队列:null
    经过堆内存不足触发GC:null
    经过堆内存不足触发GC:java.lang.ref.SoftReference@4b85612c

经过-Xmx10m设置咱们堆内存大小为10,方便构造堆内存不足的状况。能够看见咱们输出的状况咱们手动调用System.gc并无回收咱们的软引用所指向的对象,只有在内存不足的状况下才能触发。

2.2.2软引用的应用

在SoftReference的doc中有这么一句话:

Soft references are most often used to implement memory-sensitive
caches

也就是说软引用常常用来实现内存敏感的高速缓存。怎么理解这句话呢?咱们知道软引用他只会在内存不足的时候才触发,不会像强引用那用容易内存溢出,咱们能够用其实现高速缓存,一方面内存不足的时候能够回收,一方面也不会频繁回收。在高速本地缓存Caffeine中实现了软引用的缓存,当须要缓存淘汰的时候,若是是只有软引用指向那么久会被回收。

2.3 弱引用

弱引用在Java中使用WeakReference来定义一个弱引用,上面咱们说过他比软引用更加弱,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收。这里咱们就很少废话了,直接上例子:

public static void main(String[] args)  {
        WeakReference weakReference = new WeakReference(new User("zhangsan",24));
        System.gc();
        System.out.println("手动触发GC:" + weakReference.get());
    }
输出结果:
手动触发GC:null

能够看见上面的例子只要垃圾回收一触发,该对象就被回收了。

2.3.1 弱引用的做用

在WeakReference的注释中写到:

Weak references are most often used to implement canonicalizing
mappings.

从中能够知道弱引用更多的是用来实现canonicalizing mappings(规范化映射)。在JDK中WeakHashMap很好的体现了这个例子:

public static void main(String[] args) throws Exception {
        WeakHashMap<User, String> weakHashMap = new WeakHashMap();
        //强引用
        User zhangsan = new User("zhangsan", 24);
        weakHashMap.put(zhangsan, "zhangsan");
        System.out.println("有强引用的时候:map大小" + weakHashMap.size());
        //去掉强引用
        zhangsan = null;
        System.gc();
        Thread.sleep(1000);
        System.out.println("无强引用的时候:map大小"+weakHashMap.size());
    }
输出结果为:
有强引用的时候:map大小1
无强引用的时候:map大小0

能够看出在GC以后咱们在map中的键值对就被回收了,在weakHashMap中其实只有Key是弱引用作关联的,而后经过引用队列再去对咱们的map进行回收处理。

2.4 虚引用

虚引用是最弱的引用,在Java中使用PhantomReference进行定义。弱到什么地步呢?也就是你定义了虚引用根本没法经过虚引用获取到这个对象,更别谈影响这个对象的生命周期了。在虚引用中惟一的做用就是用队列接收对象即将死亡的通知。

public static void main(String[] args) throws Exception {
        ReferenceQueue referenceQueue = new ReferenceQueue();
        PhantomReference phantomReference = new PhantomReference(new User("zhangsan", 24), referenceQueue);
        System.out.println("什么也不作,获取:" + phantomReference.get());
    }
输出结果:
什么也不作,获取:null

在PhantomReference的注释中写到:

Phantom references are most often used for scheduling pre-mortem
cleanup actions in a more flexible way than is possible with the Java
finalization mechanism.

虚引用得最多的就是在对象死前所作的清理操做,这是一个比Java的finalization梗灵活的机制。 在DirectByteBuffer中使用Cleaner用来回收对外内存,Cleaner是PhantomReference的子类,当DirectByteBuffer被回收的时候未防止内存泄漏因此经过这种方式进行回收,有点相似于下面的代码:

public static void main(String[] args) throws Exception {
        Cleaner.create(new User("zhangsan", 24), () -> {System.out.println("我被回收了,当前线程:{}"+ Thread.currentThread().getName());});
        System.gc();
        Thread.sleep(1000);
    }
输出:
我被回收了,当前线程:Reference Handler

3 ThreadLocal

ThreadLocal是一个本地线程副本变量工具类,基本在咱们的代码中随处可见。这里就不过多的介绍他了。

3.1 ThreadLocal和弱引用的那些事

上面说了这么多关于引用的事,这里终于回到了主题了咱们的ThreadLocal和弱引用有什么关系呢?

在咱们的Thread类中有下面这个变量:

ThreadLocal.ThreadLocalMap threadLocals

ThreadLocalMap本质上也是个Map,其中Key是咱们的ThreadLocal这个对象,Value就是咱们在ThreadLocal中保存的值。也就是说咱们的ThreadLocal保存和取对象都是经过Thread中的ThreadLocalMap来操做的,而key就是自己。在ThreadLocalMap中Entry有以下定义:

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

能够看见Entry是WeakReference的子类,而这个弱引用所关联的对象正是咱们的ThreadLocal这个对象。咱们又回到上面的问题:

“threadlocal的key是弱引用,那么在threadlocal.get()的时候,发生GC以后,key是不是null?”

这个问题晃眼一看,弱引用嘛,还有垃圾回收那确定是为null,这实际上是不对的,由于题目说的是在作threadlocal.get()操做,证实其实仍是有强引用存在的。因此key并不为null。若是咱们的强引用不存在的话,那么Key就会被回收,也就是会出现咱们value没被回收,key被回收,致使value永远存在,出现内存泄漏。这也是ThreadLocal常常会被不少书籍提醒到须要remove()的缘由。

你也许会问看到不少源码的ThreadLocal并无写remove依然再用得很好呢?那实际上是由于不少源码常常是做为静态变量存在的生命周期和Class是同样的,而remove须要再那些方法或者对象里面使用ThreadLocal,由于方法栈或者对象的销毁从而强引用丢失,致使内存泄漏。

3.2 FastThreadLocal

FastThreadLocal是Netty中提供的高性能本地线程副本变量工具。在Netty的io.netty.util中提供了不少牛逼的工具,后续会一一给你们介绍,这里就先说下FastThreadLocal。

FastThreadLocal有下面几个特色:

1,使用数组代替ThreadLocalMap存储数据,从而获取更快的性能。(缓存行和一次定位,不会有hash冲突)

2,因为使用数组,不会出现Key回收,value没被回收的尴尬局面,因此避免了内存泄漏。

总结

文章开头的问题,为何会被问出来,实际上是对弱引用和ThreadLocal理解不深致使,不少时候只记着一个若是是弱引用,在垃圾回收时就会被回收,就会致使把这个观念先入为主,没有作更多的分析思考。因此你们再分析一个问题的时候仍是须要更多的站在不一样的场景上作更多的思考。

相关文章
相关标签/搜索