彻底搞懂java中的四种引用与ThreadLocal原理

前言

“彻底搞懂”对于本文而言会不会夸大其词了。为什么本文标题会采用这标题?

因为我实在受不了了,网上千篇一律的讲解,“抄”风巨严重,气人的是抄得都讲不清。真的是越查越气!

本文会以极其简洁的文字来讲解!

四种引用

1- 强引用

1.形式:

 String s="watermelonhit";

2.图解:
image.png

3.说明:
无论内存够不够,只要存在强引用就不会被GC(垃圾收集器)回收掉。

2- 软引用

1.形式:

SoftReference<String> sr= new SoftReference<String>(new String("watermelonhit"));

2.图解:
image.png

3.说明
当内存不足时,软引用对象就被GC(垃圾回收器)回收掉。 即,字符串会被回收掉。

4.应用:
可以用于对内存敏感的缓存。内存够时,就存放;不够就释放掉。

3- 弱引用

1.形式:

WeakReference<String> wr= new WeakReference<String>(new String("watermelonhit"));

2.图解:
image.png

3.说明:
不管内存够不够,弱引用对象都会被GC回收掉。

4.应用:
ThreadLocal的应用,其中Entry中的key变为弱引用。(后面会详细讲解)

4- 虚引用

1.形式:

 ReferenceQueue<String> queue = new ReferenceQueue<String>();
 PhantomReference<String> pr= new PhantomReference<String>("watermelonhit", queue);

2.图解:
image.png

3.说明:

  • 虚引用对象随时会被GC回收掉。
  • 与弱引用的区别是:虚引用对象被回收时其自动加入到引用队列中。
  • 使用虚引用的目的就是为了得知对象被GC的时机,从而做一些相应的处理,比如释放资源。

4.特点:
(1)必须得跟队列搭配使用;
(2)没办法通过虚引用获取到实例对象

5.应用:
堆外内存管理(当管理堆外内存的DirectByteBuffer对象被回收时,我们可通过队列获取信息,从而对堆外内存进行处理操作)

ThreadLocal

1.说明:
ThreadLocal可以实现线程拥有自己的专属值

2.形式:

public static void main(String[] args) {
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        // 线程A设置放入“wt1”
        new Thread(()->{
            threadLocal.set("wt1");
            System.out.println(Thread.currentThread().getName() + " get " + threadLocal.get());
        },"A").start();
        // 线程B设置放入“wt2”
        new Thread(()->{
            threadLocal.set("wt2");
            System.out.println(Thread.currentThread().getName() + " get " + threadLocal.get());
        },"B").start();
    }


----------输出结果-----------
A get wt1
B get wt2

3.原理:

  • set方法:
    (1)源码:
/**
     * 设置当前线程对应的ThreadLocal的值
     * @param value 将要保存在当前线程对应的ThreadLocal的值
     */
    public void set(T value) {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 判断map是否存在
        if (map != null)
            // 存在则调用map.set设置此实体entry,this这里指调用此方法的ThreadLocal对象
            map.set(this, value);
        else
            // 1)当前线程Thread 不存在ThreadLocalMap对象
            // 2)则调用createMap进行ThreadLocalMap对象的初始化
            // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
            createMap(t, value);
    }

// 其中map.set 方法如下:
private void set(ThreadLocal<?> key, Object value) {
            // 此处忽略一些非必要的代码

            ……

            // 看这一行就行了,以threadLocal为key,和value封装为Entry,然后放入map里。
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

/**
     * 获取当前线程Thread对应维护的ThreadLocalMap 
     * 
     * @param  t the current thread 当前线程
     * @return the map 对应维护的ThreadLocalMap 
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
	/**
     *创建当前线程Thread对应维护的ThreadLocalMap 
     * @param t 当前线程
     * @param firstValue 存放到map中第一个entry的值
     */
	void createMap(Thread t, T firstValue) {
        //这里的this是调用此方法的threadLocal
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

(2)讲解:
当一个线程调用ThreadLocal的set方法时,就会拿取当前线程中的map,然后以threadLocal为key,自定义值为value装入map中。

还不明白的话,可以这样通俗的理解:
每一个线程自身携带着一个大口袋,ThreadLocal就相当于一个盒子(每个线程都可以拿取这个盒子,可以理解为该盒子无限多),当线程调用ThreadLocal的set方法时,就将自定义值放入盒子中,最后再把盒子丢进自己的口袋中。

(3)图解:

e5bdf67f19b5537c069ff8710eb5506.jpg

  • get方法:
    (1)源码:
 /**
     * 返回当前线程中保存ThreadLocal的值
     * 如果当前线程没有此ThreadLocal变量,
     * 则它会通过调用{@link #initialValue} 方法进行初始化值
     * @return 返回当前线程对应此ThreadLocal的值
     */
    public T get() {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null) {
            // 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 对e进行判空 
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 获取存储实体 e 对应的 value值,即为我们想要的当前线程对应此ThreadLocal的值
                T result = (T)e.value;
                return result;
            }
        }
        /*
        	初始化 : 有两种情况有执行当前代码
        	第一种情况: map不存在,表示此线程没有维护的ThreadLocalMap对象
        	第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry
         */
        return setInitialValue();
    }

 /**
     * 初始化
     * @return the initial value 初始化后的值
     */
    private T setInitialValue() {
        // 调用initialValue获取初始化的值
        // 此方法可以被子类重写, 如果不重写默认返回null
        T value = initialValue();
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 判断map是否存在
        if (map != null)
            // 存在则调用map.set设置此实体entry
            map.set(this, value);
        else
            // 1)当前线程Thread 不存在ThreadLocalMap对象
            // 2)则调用createMap进行ThreadLocalMap对象的初始化
            // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
            createMap(t, value);
        // 返回设置的值value
        return value;
    }

(2)讲解:
当线程调用threadLocal中的get方法时,会拿取当前线程中的map,然后以threadLocal为key去获取value。(具体细节可以查看源码)

内存泄露问题

1.上述map中存放的是entry,那entry中的key是以什么形式的引用存在?

【ans】entry中的key采用了弱引用形式。
【分析】源码如下:

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

            Entry(ThreadLocal<?> k, Object v) {
                // 继承父类构造器,实现采用弱引用实现threadLocal
                super(k);
                value = v;
            }
        }

2.为什么entry中的key要采用弱引用,不能强引用吗?

【ans】防止内存泄露,采用强引用的话,会导致内存泄露;
【分析】
假设有以下代码:

public static void main(String[] args) {
        ThreadLocal<String> tl = new ThreadLocal<>();
        tl.set("wt");
        System.out.println(Thread.currentThread().getName() + " get " + tl.get());
        tl=null;
    }

(1)若key采用强引用,其引用关系图如下:
image.png

当tl=null时会发生什么?
由于key采用了强引用,故会导致ThreadLocal对象实例没法被回收掉。
image.png

若key才用弱引用的话,当ThreadLocal只剩被弱引用所指引时(即tl=null)则ThreadLocal随时可被GC回收掉。

3.key采用弱引用就不会发生内存泄露吗?

【ans】还是会的,有可能会发生value内存泄露。
【分析】继上述分析,当ThreadLocal被回收后,即外界没法获取key值(上述因为tl改变了指向,导致ThreadLocal的地址位置丢失了),也即value永远无法被获取到了,导致value所指向的实例对象(图中的"wt")没法被释放掉。

image.png

4.针对于第三问,请问存在解法方法吗?
【ans】存在,当不再使用threadLocal时,及时调用threadLocal中的remove方法,释放掉map中相应的整个entry,防止内存泄露;
【分析】
remove方法源码如下:

/**
     * 删除当前线程中保存的ThreadLocal对应的实体entry
     */
     public void remove() {
        // 获取当前线程对象中维护的ThreadLocalMap对象
         ThreadLocalMap m = getMap(Thread.currentThread());
        // 如果此map存在
         if (m != null)
            // 存在则调用map.remove
            // 以当前ThreadLocal为key删除对应的实体entry
             m.remove(this);
     }

图解:

image.png

最后

感谢读者能读到最后,希望对您有所帮助!
感言:写一篇好文章真的很幸苦,本文前前后后包括整理资料、绘制图片、编写文章等共花费了四五个小时。其中最花费时间的是如何更好地表达才能让读者更加容易的理解与吸收!
我是wt!争取做一个有输出的boy!


本文到此结束!
参考资料:
1.ThreadLocal 详解
2.强,软,弱,虚!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值