ThreadLocal理解及其内存泄露

ThreadLocal理解及其内存泄露

在这里插入图片描述

  • ThreadLocal可以理解为,为一个线程隔离的变量,他不是一个集合,他只是一个类,这个类可以set,get ,remove

  • set时使用的是哪个线程,get时,也必需用哪个线程去获取,才能获取到值

  • set的值,同时只能存在一个,后面set的值,会把前面的给覆盖了

示例代码1 证明一个 ThreadLocal只能保存一个对象

 public static void main(String[] args) throws IOException {
        ThreadLocal<Person> threadLocal = new ThreadLocal<>();
        threadLocal.set(new Person("ysh"));
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        Person person = threadLocal.get();
        System.out.println("person = " + person);

        System.out.println("______________________");
        threadLocal.set(new Person("hua"));
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        Person person1 = threadLocal.get();
        System.out.println("person1 = " + person1);
    }

输出:

person = Person{name='ysh'}
______________________
person1 = Person{name='hua'}

可以看到,一个ThreadLocal只能设置一个,当一个新的值设置进来的时候,他就会被覆盖了。

示例代码2 证明ThreadLocal是线程私有的

public static void main(String[] args) throws IOException {
        ThreadLocal<Person> threadLocal = new ThreadLocal<>();

        new Thread("t1") {
            @Override
            public void run() {
                threadLocal.set(new Person("ysh"));
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                Person person = threadLocal.get();
                System.out.println("t1 get = " + person);
            }
        }.start();

        new Thread("t2") {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                Person person1 = threadLocal.get();
                System.out.println("t2 get = " + person1);
            }
        }.start();

        System.in.read();
    }

输出结果:

t1 get = Person{name='ysh'}
t2 get = null

上面代码的意思是,在t1线程中,设置了一个person对象,在t2线程中去获取,因为,t2线程获取的时候,是等了2秒,所以,肯定t2在t1后面获取,但从输出结果来看,在t2线程中获取不到t1设置的值。

源码分析

调用 threadLocal.set(new Person(“ysh”))时,会调用下面的set方法

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

可以看到,getMap的入参是一个线程,而且是当前线程。下面是getMap的源码:

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

可见,getMap当前这个t线程的threadLocals属性给返回了。什么是threadLocals呢?

threadLocals

打开Thread的源码,可以看到

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
在这里插入图片描述

Thread类中有一个属性叫 threadLocals 的,说明,每个Thread线程都有自己一个单独的Map,这个类型是 ThreadLocal.ThreadLocalMap ,如下:

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

这个threadLocals的意思是,每个线程可以存入自己这个线程私有的很多个ThreadLocal,键是ThreadLocal类型,值是传递进来的任意对象

那么,这个ThreadLocal.ThreadLocalMap又是什么呢?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
在这里插入图片描述

从上图可以看出,这个 ThreadLocalMap其实是一个 ThreadLocal的一个静态内部类,其中,重要结构是在上图也能看到,是一个Entry的类型,那么,什么又是 Entry我们一会再看。

现在先看看,当我们调用 ThreadLocalMap时,都干了什么

map.set(this, value);

set方法中,获取每个线程中的ThreadLocalMap,然后,将 threadLocal 对象作为键,将Person对象作为值,传递给了 map.set(this, value); 方法,在这个方法中,重要的是调用了下面这行代码

tab[i] = new Entry(key, value);

其中,key和value的值见下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传在这里插入图片描述

key就是自己new 的ThreadLocal对象,键是Person对象

保存在每个线程单独的map中。

可以看到,这里new 了一个Entry,那么到底什么是Entry呢???

Entry

要说Entry,让我们来先想一下HashMap,他是我们最常用的一个Map类型,他内部的结构是一个个的Node,每当传入一个key和value时,都要进行hashcode,去计算每个value放在index的位置,随着数量的增加,会自动扩容,后面还会变成红黑树的结构,这里就不再展开了(其实也忘的差不多了)

总之,这个Entry,就是类比HashMap中的每一个Node,每个Entry中保存着一个k和一个v

然后,保存这个Entry结构的类,就是上面说的 ThreadLocalMap,从后缀可以看到,他就是一种map。只是内部的每个结点使用的是全新的Entry结构。

仔细看,这个Entry 是 ThreadLocalMap 的一个静态内部类,他继承 WeakReference。是一个弱引用。

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

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

既然他是一个弱引用,那弱引用是一种 只要JVM gc,就一定会被回收的的一种引用,到这里,先不说为什么要使用弱引用,先来总结一下。

小总结

每一个Thread线程中有自己一个单独的map,这个map是ThreadLocalMap,键是ThreadLocal,值是任意类型。然后,这个ThreadLocalMap其内部的每个结点的数据结构是一个个的Entry,Entry是一个弱引用。

问1:为什么ThreadLocal可以做到线程隔离??

因为,使用ThreadLocal对象调用其set方法的时候,他将值保存到每个线程单独的ThreadLocalMap当中,所以,是线程隔离。

问2:为什么在同一个线程中重新调用ThreadLocal对象的set方法重置新的值,为覆盖旧的值呢?

这个很好理解吧,类比HashMap,当设置同一个key时,后面的会把前面的值给覆盖掉,这里,因为键是 ThreadLocal对象,所以,在同一个线程中,使用同一个ThreadLocal对象的set方法是时,就是设置到了同一个map中,就会导致覆盖。

接下来,让我们来想想为什么Entry内部要使用弱引用?

为什么要弱引用

先上图:我从知乎上偷的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传在这里插入图片描述

弱引用是一种 只要JVM gc,就一定会被回收的的一种引用

假设使用强引用

假设Entry中的key使用的是强引用,那么 ,就存在这种情况,当threadLocal对象使用下面代码创建后,

ThreadLocal<Person> threadLocal = new ThreadLocal<>();

当threadLocal变量被回收时,threadLocal=null,左侧的实线就断了,但因为真正的保存 ThreadLocal在堆中,因为其还有强引用指向Entry中的key,导致还无法被回收。导致内存泄露。

那其内部使用了 弱引用就一定不会内存泄露吗?

答案是 : 不一定

如果一个ThreadLocal不存在外部强引用时,ThreadLocal势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉。

但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:

Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value

永远无法回收,造成内存泄漏。

那么,说 了这么多,ThreadLocal的正确使用方法是什么呢?

ThreadLocal正确的使用方法

  • 每次使用完ThreadLocal都调用它的remove()方法清除数据
  • 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。
private static ThreadLocal<Person> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) throws IOException {

        new Thread("t1") {
            @Override
            public void run() {
                threadLocal.set(new Person("ysh"));
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                Person person = threadLocal.get();
                System.out.println("t1 get = " + person);
                //  使用完毕,一定要调用remove方法
                threadLocal.remove();
            }
        }.start();
    }

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值