ThreadLoacl详解(使用+原理+ThreadLoacl造成的内存泄漏)

1.什么是ThreadLocal

ThreadLocal,线程本地化, 可以私有化存储线程的变量值。可以简单理解为ThreadLoacl能为每个线程提供一个专属于自己的存储空间, 可以在整个线程存活的过程中随时取用,极大地方便了一些逻辑的实现。

常见用法包括:

  • 存储单个线程上下文信息。比如存储id等;
  • 使变量线程安全。变量既然成为了每个线程内部的局部变量,自然就不会存在并发问题了;
  • 减少参数传递。比如做一个trace工具,能够输出工程从开始到结束的整个一次处理过程中所有的信息,从而方便debug。由于需要在工程各处随时取用,可放入ThreadLocal。

2.实现原理

在Thread的类中,会有一个属性Map (java.lang.Thread#threadLocals),

在这里插入图片描述

每当我们调用ThreadLoacl的set/get方法,其实就是在当前线程对应的Map中,以ThreadLoacl为key,存或者取对应的value。

在这里插入图片描述

代码示例:

 public static void main(String[] args) throws InterruptedException {
        ThreadLocal<Integer> tl1 = new ThreadLocal<>();
        tl1.set(1);
        ThreadLocal<String> tl2 = new ThreadLocal<>();
        tl2.set("main");
        System.out.println("main线程:"+tl1.get());
        System.out.println("main线程:"+tl2.get());
        new Thread(()->{
            tl1.set(2);
            tl2.set("thread");
            System.out.println("线程2:"+tl1.get());
            System.out.println("线程2:"+tl2.get());
        }).start();
        Thread.sleep(100L);
        System.out.println("main线程睡醒后:"+tl1.get());
        System.out.println("main线程睡醒后:"+tl2.get());

 }

运行结果:

在这里插入图片描述

可以看到虽然,两个线程对同一个ThreadMap对象做了操作,但是没有相互影响。

3.源码解析:

看看ThreadLocal的源码,是否和我们刚刚说的实现原理一致

ThreadLocal的set方法源码 :

//源码中的set方法 
//java.lang.ThreadLocal#set()
public void set(T value) {
    //1.获取当前操作这个ThreadLoacl的线程
    Thread t = Thread.currentThread();
    //2.获取当前线程的属性:map
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //3—1.非空,直接对map进行设置
        //此时key为threadLoacl的实例对象,value为传值
        map.set(this, value);
    else
        //3-2.空,调用创建方法,同时设值
        createMap(t, value);
}

 //获取线程中的属性:java.lang.Thread#threadLocals
 ThreadLocalMap getMap(Thread t) {
     return t.threadLocals;
 }

 void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
 }

ThreadLocal的get方法源码 :

//源码中的get方法 
//java.lang.ThreadLocal#get()
public T get() {
    //1.获取当前操作这个ThreadLoacl的线程
    Thread t = Thread.currentThread();
    //2.获取当前线程的属性:map
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //3-1.map不为空,直接调用get方法获取value
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //3-2.为空 调用创建方法,并返回null
    return setInitialValue();
}

private T setInitialValue() {
    //此方法返回null
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

protected T initialValue() {
    return null;
}

可以看到ThreadLocal的实现源码,符合我们前文所描述的实现原理。

4.ThreadLoacl与内存泄漏问题

一句话概括: 本该回收的无用对象没有得到回收 。

简而言之,jvm中有一些对象已经属于无用的垃圾了,但是但是由于某些原因,导致gc无法清除它。

ThreadLoacl可能会造成内存泄漏的原因:

我们将ThreadLoacl本身作为key。存放在Thread的Map中,那么在此线程的存活时间内,该ThreadLoacl对象都会被该线程的Map所持有,那么即使这ThreadLoacl对象不在使用了,gc也不会对该对象进行回收,所以很容易就造成了内存泄漏。

ThreadLoacl对内存泄漏的自身解决方法

刚刚提到的问题ThreadLoacl也想到了,所以我们看ThreadLoacl.ThreadLocalMap的内部实现:

在这里插入图片描述

可以看到,该Map的Entry继承了WeakReference,它是一个弱引用

WeakReference如字面意思,弱引用。那么也就是说,ThreadLocalMap中保存的Entry.key实际上是一个弱引用对象(这句话可能需要对map源码熟悉的人才能看懂),而弱引用对象,其所指向的对象在GC的时候是会被回收掉。 (详见:java四种引用类型详解

结合上文实例代码分析

  • 如果使用强引用:,当ThreadLocal对象(假设为ThreadLocal@123)的引用(即:tl1,是一个强引用,指向ThreadLocal@123)被回收了,ThreadLocalMap本身依然还持有ThreadLocal@123的强引用,如果没有手动删除这个key,则ThreadLocal@123不会被回收,所以只要当前线程不消亡,ThreadLocalMap引用的那些对象就不会被回收,可以认为这导致Entry内存泄漏。

  • 如果使用弱引用,那指向ThreadLocal@123对象的引用就两个:tl1强引用,和ThreadLocalMap.Entry 中key的弱引用。一旦tl1被回收,则指向ThreadLocal@123的就只有弱引用了,在下次gc的时候,这个ThreadLocal@123就会被回收。

看到这里我们还会有疑问,ThreadLocalMap.Entry的key对象被回收了,此时key变成了null,但是我们是没法访问这些Entry的,但由于Entry中还保存着value,value可能还占用着一大块内存,那么这部分内存就相当于内存泄漏了,因为他占用着内存没法回收,而我们又没法访问到它。

对此在调用ThreadLocal的处理方法时,在调用get()/set()/remove()方法的时候都会清除线程ThreadLocalMap里所有key为null的value。这些举动不能保证内存就一定会回收,因为可能这条线程被放回到线程池里后再也没有使用,或者使用的时候没有调用其get()/set()/remove()方法。
所以我们日常使用时要注意,在 ThreadLocalMap 已经确定不需要在使用时,可以手动调用 remove()方法。

ThreadLoacl可能内存泄漏归根结底是由于ThreadLocalMap的生命周期跟Thread一样长。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值