ThreadLocal终于理解了为什么会内存泄露(看马士兵视频有感)

首先 介绍一下四个引用:

  1. 强:new出来的或是反射出来的对象,不会因为内存不足而被回收,除非不使用了。
  2. 软:在内存不足时会回收这个内存,主要用于缓存。
  3. 弱:在gc时会回收这个内存。
  4. 虚:在gc时回收这个内存并且和上报一个消息通知。

下面就是ThreadLocal的一个例子:

 ThreadLocal<test> tl = new ThreadLocal<>();
        new Thread(()->{
            tl.set(new test());

        }).start();
        new Thread(()->{
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            test test = tl.get();
            System.out.println(test);
        }).start();

这是一个很简单的例子,下面看一下结果:
null

为什么呢?
来看一下set函数吧:

public void set(T value) {
     //获取当前线程
        Thread t = Thread.currentThread();
       //通过当前线程拿去threadlocalmap。
      /*
			ThreadLocalMap getMap(Thread t) {
			         //可以看见拿取的就是当前线程的一个成员变量,怪不得是线程隔绝的,所以上面的结果为null。
      				 return t.threadLocals;
    		}
		*/
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    //这是一个map,类似于hashmap,不过数组是一个Entry数组,元素是一个Entry对象。
    private void set(ThreadLocal<?> key, Object value) {
    
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            /*从这里可以看出来Entry继承的是一个弱引用类,且将key传递到了父类中
				static class Entry extends WeakReference<ThreadLocal<?>> {
		            /** The value associated with this ThreadLocal. */
		            Object value;
		            Entry(ThreadLocal<?> k, Object v) {
		                super(k);
		                value = v;
		            }
		        }
			*/
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

好了,现在终于明了了,或许可以再细说一下弱引用,如图:
使用代码:WeakReference weak = new WeakReference(new test());
在这里插入图片描述
weak指向的WR是一个强引用,但是WR里又有一个弱引用指向了new test()对象,所以在垃圾回收器清理时清理的是new test()而不是weak。所以这样看是不是一目了然了呢。
再来一个图,来表示threadlocal的原理图:
在这里插入图片描述
在每一个线程的threadlocalmap中有很多的Entry条目,每一个条目里包含的都是key和value。其中k是一个弱引用,在这个弱引用里包含了一个Threadlocal。而此时的tl是强引用指向了Threadlocal,所以此时不会发生问题,即使gc发生也不会,但是如果tl被置空了,那么就表示key中的包含的threadlocal只有一个弱引用指向了,那么在垃圾回收器来回收时会将这个key回收掉,注意此时的这个key指的是传递给了父类的ThreadLocal,如下代码:

 Entry(ThreadLocal<?> k, Object v) {
  	super(k);
     value = v;
}
public WeakReference(T referent) {
        super(referent);
    }
Reference(T referent) {
       this(referent, null);
}
 Reference(T referent, ReferenceQueue<? super T> queue) {
     this.referent = referent;
     this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
 }

所以如果ThreadLocal被回收了的话那么就表示此时的key直接为null了。那么此时就会使一个null指向了test对象,即此时内存泄漏发生了。所以此时的建议是将为空的条目手动remove掉。

对此需要解答的是ThreadLocal为什么在key上面使用弱引用:

  1. 如果使用的时强引用,那么即使tl为null了,但是key指向了ThreadLocal对象,就使得ThreadLocal没办法在这个线程就结束前释放掉,那么这个内存就是一个没办法被访问到的内存了,从而发生内存泄漏。所以这里不能使用强引用。
  2. 为什么使用弱引用而不是其他的呢,如果是软引用那么除非内存不够发生gc,否则这片内存一直在,不好,虚引用作用的时对外内存,都不适合。所以最适合的就是弱引用了。当tl被置空了,那么就只有一个弱引用指向了ThreadLocal了,那么当发生gc时就直接回收了。但是这个也会产生问题就是entry中的value的内存泄露问题,所以此时的做法是将key为null的条目remove掉。

为了验证上面的理论做了个实验验证一下(不是马士兵视频的内容,是自实现的):

    static ThreadLocal<test> tl = new ThreadLocal<>();
    public  static void main( String[] args ) throws NoSuchFieldException {
        new Thread(()->{
            test test1 = new test();
            tl.set(test1);
            tl = null;
            System.out.println("此时的tl被指控,但是还没开始gc,查看key之还在不在");
            System.out.println(returnWeakRefrenceValue());
            System.out.println("开始gc。。。。");
            System.gc();
            System.out.println("gc完毕,查看key还在不在");
            System.out.println(returnWeakRefrenceValue());
        }).start();
    }
    static Object returnWeakRefrenceValue(){
        Object value = null;
        Thread thread = Thread.currentThread();
        Class<? extends Thread> aClass = thread.getClass();
        try {
            //由于是内部类且不是public的所以无法通过正常手段拿到,所以只能反射了。
            Field threadLocals = aClass.getDeclaredField("threadLocals");
            threadLocals.setAccessible(true);
            Object o = threadLocals.get(thread);
            Class<?> aClass1 = o.getClass();
            //拿取到map中的table属性即Entry的数组
            Field table = aClass1.getDeclaredField("table");
            table.setAccessible(true);
            Object[] o1 = (Object[]) table.get(o);
            System.out.println("Entry对象数组:   "+o1+"    "+o1.length);
            //遍历一下数组,将有值的取出来,因为是通过hashcode来定位的,这里偷了个懒,直接暴力了。
            for (int i = 0; i < o1.length; i++) {
                Object o2 = o1[i];
                if (o2!=null) {
                    //此时拿取到的对象就是具体的Entry了,但是具体的值放在了父类的父类的Refrence中,所以需要到父类里拿去数据,从上面的源码中也可以看出来这个继承关系。
                    Class<?> aClass2 = o2.getClass();
                    Class<?> superclass = aClass2.getSuperclass().getSuperclass();
                    //将referent属性拿取出来
                    Field referent = superclass.getDeclaredField("referent");
                    referent.setAccessible(true);
                    value = referent.get(o2);
                    Field fields = aClass2.getDeclaredField("value");
                    fields.setAccessible(true);
                    Object o3 = fields.get(o2);
                    System.out.println("每一个条目的value值:    "+fields+"    "+o3);
                    System.out.println("######################我是分割线############################");
                    break;
                }
            }
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return value;
    }

结果展示:

此时的tl被置null,但是还没开始gc,查看key之还在不在
Entry对象数组: [Ljava.lang.ThreadLocal T h r e a d L o c a l M a p ThreadLocalMap ThreadLocalMapEntry;@404acb95 16
每一个条目的value值: java.lang.Object java.lang.ThreadLocal T h r e a d L o c a l M a p ThreadLocalMap ThreadLocalMapEntry.value java_common_test.test@3487fff4
######################我是分割线############################
java.lang.ThreadLocal@22078c92
开始gc。。。。
gc完毕,查看key还在不在
Entry对象数组: [Ljava.lang.ThreadLocal T h r e a d L o c a l M a p ThreadLocalMap ThreadLocalMapEntry;@404acb95 16
每一个条目的value值: java.lang.Object java.lang.ThreadLocal T h r e a d L o c a l M a p ThreadLocalMap ThreadLocalMapEntry.value java_common_test.test@3487fff4
######################我是分割线############################
null

好了,至此ThreadLocal理解了。
如有不对的地方请不吝指教,谢谢!!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值