侧边栏壁纸
  • 累计撰写 101 篇文章
  • 累计创建 89 个标签
  • 累计收到 9 条评论

JUC线程局部变量之ThreadLocal浅谈

bearjun
2021-11-25 / 0 评论 / 0 点赞 / 1,595 阅读 / 3,994 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2021-11-25,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

ThreadLocal是什么

ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。

只要线程活动并且可访问Threadlocal实例,每个线程都会对其线程局部变量的副本保持隐式引用;一个线程消失后,其线程本地实例的所有副本都会受到垃圾收集(除非存在对这些副本的其他引用)。

这个和前面的JMM很像,只是volatile是每个线程的本地缓存会快速同步到主内存,而ThreadLocal始终保持每个线程的变量只是自己持有。
30765-ohfklsmqwga.png

ThreadLocal能做什么

 实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份),主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题。

ThreadLocal基本就是解决线程自己的变量不共享的问题。像最典型的就是有过滤器链或者拦截器链的时候。我们在前几层链的时候需要传递某个参数在后面的链中获取前面链中的内容。当然,还有一些数据初始话的解决方案。
大家可以看看bearweb管理系统中Spring Security登陆链中怎么把失败的信息记录到日志。

ThreadLocal基本使用

ThreadLocal基本就这么几个API,我们一起来看看:

  • T get() 返回该线程局部变量的当前线程副本中的值。
  • void remove() 删除该线程局部变量的当前线程值。
  • void set(T value) 将该线程局部变量的当前线程副本设置为指定值。
  • static ThreadLocal withInitial(Supplier supplier) 创建一个线程局部变量设置初始值。
public static void main(String[] args) throws InterruptedException {
        ThreadLocal<String> s = new ThreadLocal<>();
        ThreadLocal<String> initS = ThreadLocal.withInitial(() -> "bear");

        s.set("bearjun");

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ":" + s.get());
            System.out.println(Thread.currentThread().getName() + ":" + initS.get());
        }, "线程1").start();

        TimeUnit.SECONDS.sleep(1);

        System.out.println(Thread.currentThread().getName() + ":" + s.get());
        System.out.println(Thread.currentThread().getName() + ":" + initS.get());

        // 最终打印结果
        /**
         * 线程1:null
         * 线程1:bear
         * main:bearjun
         * main:bear
         */
    }

通过上面的代码我们可以很清楚的看清:ThreadLocal只是线程自己的局部变量可以获取,但是初始化是所有的线程都可以获取的(初始化后不对变量做任何操作)。
然后我们的代码写的没有问题吗?不对,还有一个方法没用到,那就是remove()。下面是阿里开发手册规定的:

94870-rmgy49dt3qq.png

ThreadLocal源码分析

基本源码分析

ThreadLocal在java.lang包下面,我们先来看看源码:

// ThreadLocal 变量threadLocals
ThreadLocal.ThreadLocalMap threadLocals = null;

// ThreadLocal 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);
}

// ThreadLocal get方法
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

// ThreadLocal remove方法
public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
 }

// ThreadLocal getMap()方法
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

通过源码我们不难看出来,ThreadLocal里面有个变量ThreadLocalMap,不管是get/set,直接获取到ThreadLocalMap,然后把线程当做key,传入的值做value。放入到ThreadLocalMap中。

Thread、ThreadLocal、ThreadLocalMap的关系

通过源码,我们不难发现,ThreadLocal里面有个变量叫ThreadLocalMap,而在get获取值的时候,又要从ThreadLocalMap中的变量Entry中获取值。那三者到底什么关系呢?

92336-tkqlvx4c9fd.png

ThreadLocalMap实际上就是一个以ThreadLocal实例为key,任意对象为value的Entry对象。当我们为ThreadLocal变量赋值,实际上就是以当前ThreadLocal实例为key,值为value的Entry往这个threadLocalMap中存放。
建议亲自翻一下源码,看的更清楚。

ThreadLocal内存泄露问题

前面阿里规范也说了:必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用try-finally块进行回收。

那么问题来了?

  • 问题1:什么是内存泄漏?
  • 问题2:谁惹的祸?
  • 问题3:为什么要用弱引用?不用如何?

什么是内存泄漏?

不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。

谁惹的祸?

我们先来看看ThreadLocalMap的具体源码:

81461-vm4k0w4a9ah.png
我们可以看到,ThreadLocalMap中的Entry居然采用了弱引用。

我们先来说说引用吧:

  • 强引用(默认支持模式):当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收。
  • 软引用:当内存充足时,它不会被回收;相反的,当内存不足它会被回收。
  • 弱引用:只要垃圾回收机制(gc)一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。 
  • 虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列 (ReferenceQueue)联合使用。

那我们接着说为什么使用弱引用:
当我们为ThreadLocal变量赋值,实际上就是当前的Entry(ThreadLocal实例为key,值为value)往这个ThreadLocalMap中存放。Entry中的key是弱引用,当ThreadLocal外部强引用被置为null(tl=null),那么当系统GC的时候,根据可达性分析,这个ThreadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收。

这样就可以万事大吉了吗?当然不是。
这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

所以最后总结最后一句:ThreaLocal用完一定记得手动remove。

总结

  • ThreadLocal并不解决线程间共享数据的问题,而只是适用于变量在线程间隔离且在方法间共享的场景。

  • ThreadLocal通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题。每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题。

  • ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题。用完一定记得remove。

0

评论区