百度二面:ThreadLocal 传参如何使用?

业余草

共 11826字,需浏览 24分钟

 ·

2021-09-12 11:53

你知道的越多,不知道的就越多,业余的像一棵小草!

你来,我们一起精进!你不来,我和你的竞争对手一起精进!

编辑:业余草

cnblogs.com/aspirant/p/8991010.html

推荐:https://www.xttblog.com/?p=5279

ThreadLocal 也可以跟踪一个请求,从接收请求,处理请求,到返回请求,只要线程不销毁,就可以在线程的任何地方,调用这个参数。

这两天,粉丝群一位网友分享了百度二面的题目:如何使用 ThreadLocal 传参,引起了众多群友的讨论,其中有不少网友的回答很有深度。这里我给大家分享一篇文章,看完希望对大家能有所突破。如有不懂,或疑问,欢迎加我微信:codedq,进行粉丝群进行技术交流!

我先对 ThreadLocal 做个小总结:

  • JVM 利用设置 ThreadLocalMap 的 Key 为弱引用,来避免内存泄露。
  • JVM 利用调用 remove、get、set 方法的时候,回收弱引用。
  • 当 ThreadLocal 存储很多 Key 为 null 的 Entry 的时候,而不再去调用 remove、get、set 方法,那么将导致内存泄漏。
  • 当使用 static ThreadLocal 的时候,延长 ThreadLocal 的生命周期,那也可能导致内存泄漏。因为,static 变量在类未加载的时候,它就已经加载,当线程结束的时候,static 变量不一定会回收。那么,比起普通成员变量使用的时候才加载,static 的生命周期加长将更容易导致内存泄漏危机。

「事实上,在 ThreadLocalMap 中的 set/getEntry 方法中,会对 key 为 null(也即是 ThreadLocal 为 null)进行判断,如果为 null 的话,那么是会对 value 置为 null 的。我们也可以通过调用 ThreadLocal 的 remove 方法进行释放!」

threadlocal 里面使用了一个存在弱引用的 map,当释放掉 threadlocal 的强引用以后,map 里面的 value 却没有被回收。而这块 value 永远不会被访问到了。所以存在着内存泄露。最好的做法是将调用 threadlocal 的 remove 方法。

在 threadlocal 的生命周期中,都存在这些引用。看下图: 实线代表强引用,虚线代表弱引用。

threadlocal 的生命周期

每个 thread 中都存在一个 map, map 的类型是 ThreadLocal.ThreadLocalMap。Map 中的 key 为一个 threadlocal 实例。这个 Map 的确使用了弱引用,不过弱引用只是针对 key。每个 key 都弱引用指向 threadlocal。当把 threadlocal 实例置为 null 以后,没有任何强引用指向 threadlocal 实例,所以 threadlocal 将会被 gc 回收。但是,我们的 value 却不能回收,因为存在一条从 current thread 连接过来的强引用。只有当前 thread 结束以后,current thread 就不会存在栈中,强引用断开,Current Thread, Map, value 将全部被 GC 回收。

所以得出一个结论就是只要这个线程对象被 gc 回收,就不会出现内存泄露,但「在threadLocal 设为 null 和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露」。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。「比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露」

PS:Java为了最小化减少内存泄露的可能性和影响,在 ThreadLocal 的 get,set 的时候都会清除线程 Map 里所有 key 为 null 的 value。所以最怕的情况就是,threadLocal 对象设 null 了,开始发生“内存泄露”,然后使用线程池,这个线程结束,线程放回线程池中不销毁,这个线程一直不被使用,或者分配使用了又不再调用 get,set 方法,那么这个期间就会发生真正的内存泄露。

一、目录

  1. ThreadLocal 是什么?有什么用?

  2. ThreadLocal 源码简要总结?

  3. ThreadLocal 为什么会导致内存泄漏?

二、ThreadLocal 是什么?有什么用?

引入话题:在并发条件下,如何正确获得共享数据?举例:假设有多个用户需要获取用户信息,一个线程对应一个用户。在 mybatis 中,session 用于操作数据库,那么设置、获取操作分别是 session.set()、session.get(),如何保证每个线程都能正确操作达到想要的结果?

/*
 * 回顾 synchronized 在多线程共享线程的问题
 */

public class ThreadLocalOne {
     volatile Person person=new Person();
 
     public  synchronized String setAndGet(String name){
          //System.out.print(Thread.currentThread().getName()+":");
           person.name=name;
           //模拟网络延迟
           try {
                TimeUnit.SECONDS.sleep(2);
           } catch (InterruptedException e) {
                e.printStackTrace();
           }
           return person.name;
     }
 
     public static void main(String\[\] args) {
           ThreadLocalOne  threadLocal\=new ThreadLocalOne();
           new Thread(()->System.out.println(threadLocal.setAndGet("arron")),"t1").start();
           new Thread(()->System.out.println(threadLocal.setAndGet("tony")),"t2").start();
     }
}
 
class Person{
     String name\="tom";
     public Person(String name) {
           this.name=name;
     }
 
     public Person(){}
}

运行结果:

无synchronized:
t1:tony
t2:tony

有synchronized:
t1:arron
t2:tony

步骤分析:

  1. 无 synchronized 的时候,因为非原子操作,显然不是预想结果,可参考我关于 synchronized 的讨论。
  2. 现在,我们的需求是:每个线程独立的设置获取 person 信息,不被线程打扰。
  3. 因为,person 是共享数据,用同步互斥锁 synchronized,当一个线程访问共享数据的时候,其他线程堵塞,不再多余赘述。

通过举例问题,可能大家又会很疑惑?

mybatis、hibernate 是如何实现的呢?

synchronized 不会很消耗资源,当成千上万个操作的时候,承受并发不说,数据返回延迟如何确保用户体验?

ThreadLocal 是什么?有什么用?

/**
 * 谈谈 ThreadLocal 的作用
 */

public class ThreadLocalThree {
     ThreadLocal<Person> threadLocal=new ThreadLocal<Person>();
     public String setAndGet(String name){
           threadLocal.set(new Person(name));
           try {
                TimeUnit.SECONDS.sleep(2);
           } catch (InterruptedException e) {
                e.printStackTrace();
           }
           return threadLocal.get().name;
     }
 
     public static void main(String[] args) {
           ThreadLocalThree  threadLocal=new ThreadLocalThree();
           new Thread(()->System.out.println("t1:"+threadLocal.setAndGet("arron")),"t1").start();
           new Thread(()->System.out.println("t2:"+threadLocal.setAndGet("tony")),"t2").start();
     }
}

运行结果:

t1:arron
t2:tony

分析:

1、根据预期结果,那 ThreadLocal 到底是什么?

回顾 Java 内存模型

在虚拟机中,堆内存用于存储共享数据(实例对象),堆内存也就是这里说的主内存。

每个线程将会在堆内存中开辟一块空间叫做线程的工作内存,附带一块缓存区用于存储共享数据副本。那么,共享数据在堆内存当中,线程通信就是通过主内存为中介,线程在本地内存读并且操作完共享变量操作完毕以后,把值写入主内存。

  1. ThreadLocal 被称为线程局部变量,说白了,他就是线程工作内存的一小块内存,用于存储数据。
  2. 那么,ThreadLocal.set()、ThreadLocal.get() 方法,就相当于「把数据存储于线程本地,取也是在本地内存读取」「就不会像 synchronized 需要频繁的修改主内存的数据,再把数据复制到工作内存,也大大提高访问效率」

ThreadLocal 到底有什么用?

  1. 回到最开始的举例,也就等价于 mabatis、hibernate 为什么要使用threadlocal 来存储 session?
  2. 作用一**:因为线程间的数据交互是通过工作内存与主存的频繁读写完成通信,然而存储于线程本地内存,提高访问效率,避免线程阻塞造成 cpu 吞吐率下降**。
  3. 作用二:「在多线程中,每一个线程都需要维护 session,轻易完成对线程独享资源的操作」

总结:

Threadlocal 是什么?在堆内存中,每个线程对应一块工作内存,threadlocal 就是工作内存的一小块内存

Threadlocal 有什么用?threadlocal 用于存取线程独享数据,提高访问效率

ThreadLocal 源码简要总结?

那有同学可能还是有点云里雾里,感觉还是没有吃透?那线程内部如何去保证线程独享数据呢?

在这里,我只做简要总结,若有兴趣,可参考文章尾部的文章链接。重点看 get、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);
}

分析:

  1. 一个线程对应一个 ThreadLocalMap ,可以存储多个 ThreadLocal 对象。
  2. ThreadLocal 对象作为key、独享数据作为value。
  3. ThreadLocalMap 可参考 HashMap,在 ThreadMap 里面存在 Entry 数组也就是一个 Entry 一个键值对。
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();
}

分析:

  1. 一个线程对应一个 ThreadLocalMap,get() 就是当前线程获取自己的 ThreadLocalMap。
  2. 线程根据使用那一小块的 threadlocal,根据 ThreadLocal 对象作为 key,去获取存储于 ThreadLocalMap 中的值。

总结:

回顾一下,我们在单线程中如何使用 HashMap 的?hashMap 根据数组 + 链表来实现 HashMap,一个 key 对应一个 value。那么,我们抽象一下, Threadlocal 也相当于在多线程中的一种 HashMap 用法,相当于对 ThradLocal 的操作也就如单线程操作一样。

总之,ThreadLocal 就是堆内存的一块小内存,它用 ThreadLocalMap 维护 ThreadLocal 对象作为 key,独享数据作为 value 的东西。

hreadLocal 为什么会导致内存泄漏?

「synchronized 是用时间换空间(牺牲时间)、ThreadLocal 是用空间换时间(牺牲空间)」,为什么这么说?

「因为 synchronized 操作数据,只需要在主存存一个变量即可,就阻塞等共享变量,而 ThreadLocal 是每个线程都创建一块小的堆工作内存」。显然,印证了上面的说法。

一个线程对应一块工作内存,线程可以存储多个 ThreadLocal。那么假设,开启 1 万个线程,每个线程创建 1 万个 ThreadLocal,也就是每个线程维护 1 万个 ThreadLocal 小内存空间,而且当线程执行结束以后,假设这些 ThreadLocal 里的 Entry 还不会被回收,那么将很容易导致堆内存溢出。

怎么办?难道 JVM 就没有提供什么解决方案吗?

ThreadLocal 当然有想到,所以他们把 ThreadLocal 里的 Entry 设置为弱引用,当垃圾回收的时候,回收 ThreadLocal。

什么是弱引用?

  1. Key 使用强引用:也就是上述说的情况,引用 ThreadLocal 的对象被回收了,ThreadLocal 的引用 ThreadLocalMap 的 Key 为强引用并没有被回收,如果不手动回收的话,ThreadLocal 将不会回收那么将导致内存泄漏。
  2. Key 使用弱引用:引用的 ThreadLocal 的对象被回收了,「ThreadLocal的引用 ThreadLocalMap 的 Key 为弱引用,如果内存回收,那么将ThreadLocalMap 的 Key 将会被回收,ThreadLocal 也将被回收。value 在ThreadLocalMap 调用 get、set、remove 的时候就会被清除」
  3. 比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:「弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除」

那按你这么说,既然 JVM 有保障了,还有什么内存泄漏可言?

ThreadLocalMap 使用 ThreadLocal 对象作为弱引用,当垃圾回收的时候,ThreadLocalMap 中 Key 将会被回收,也就是将 Key 设置为 null 的 Entry。「如果线程迟迟无法结束,也就是 ThreadLocal 对象将一直不会回收,回顾到上面存在很多线程 + TheradLocal,那么也将导致内存泄漏。(内存泄露的重点)」

其实,在 ThreadLocal 中,当调用 remove、get、set 方法的时候,会清除为 null 的弱引用,也就是回收 ThreadLocal。

ThreadLocal 提供一个线程(Thread)局部变量,访问到某个变量的每一个线程都拥有自己的局部变量。说白了,ThreadLocal 就是想在多线程环境下去保证成员变量的安全。

「ThreadLocal 提供的方法」

ThreadLocal API

ThreadLocal API

「对于 ThreadLocal 而言,常用的方法,就是 get/set/initialValue 方法。」

「我们先来看一个例子」

ThreadLocal 例子

「运行结果」

ThreadLocal 例子运行结果

是你想象中的结果么?

「很显然,在这里,并没有通过 ThreadLocal 达到线程隔离的机制,可是ThreadLocal不是保证线程安全的么?这是什么鬼?」

「虽然,ThreadLocal 让访问某个变量的线程都拥有自己的局部变量,但是如果这个局部变量都指向同一个对象呢?这个时候 ThreadLocal 就失效了。仔细观察下图中的代码,你会发现,threadLocal 在初始化时返回的都是同一个对象 a!」

看一看 ThreadLocal 源码

「我们直接看最常用的set操作:」

set操作
线程局部变量
createMap

「你会看到,set 需要首先获得当前线程对象 Thread;」

「然后取出当前线程对象的成员变量 ThreadLocalMap;」

「如果 ThreadLocalMap 存在,那么进行 KEY/VALUE 设置,KEY 就是 ThreadLocal;」

「如果 ThreadLocalMap 没有,那么创建一个;」

「说白了,当前线程中存在一个 Map 变量,KEY 是 ThreadLocal,VALUE 是你设置的值。」

「看一下 get 操作:」

get 操作

「这里其实揭示了 ThreadLocalMap 里面的数据存储结构,从上面的代码来看,ThreadLocalMap 中存放的就是 Entry,Entry 的 KEY 就是 ThreadLocal,VALUE 就是值。」

「ThreadLocalMap.Entry:」

弱引用

「在 JAVA 里面,存在强引用、弱引用、软引用、虚引用。这里主要谈一下强引用和弱引用。」

强引用,就不必说了,类似于:

A a = new A();

B b = new B();

考虑这样的情况:

C c = new C(b);  
b = null

考虑下 GC 的情况。要知道 b 被置为 null,那么是否意味着一段时间后 GC 工作可以回收 b 所分配的内存空间呢?答案是否定的,因为即便b被置为 null,但是 c 仍然持有对 b 的引用,而且还是强引用,所以 GC 不会回收 b 原先所分配的空间!既不能回收利用,又不能使用,这就造成了「内存泄露」

那么如何处理呢?

「可以 c = null;也可以使用弱引用!(WeakReference w = new WeakReference(b);)」

分析到这里,我们可以得到:

内存结构图

「这里我们思考一个问题:ThreadLocal 使用到了弱引用,是否意味着不会存在内存泄露呢?」

「首先来说,如果把 ThreadLocal 置为 null,那么意味着 Heap 中的 ThreadLocal 实例不在有强引用指向,只有弱引用存在,因此 GC 是可以回收这部分空间的,也就是 key 是可以回收的。但是 value 却存在一条从 Current Thread 过来的强引用链。因此只有当 Current Thread 销毁时,value 才能得到释放。」

「因此,只要这个线程对象被 gc 回收,就不会出现内存泄露,但在 threadLocal 设为 null 和线程结束这段时间内不会被回收的,就发生了我们认为的内存泄露。最要命的是线程对象不被回收的情况,比如使用线程池的时候,线程结束是不会销毁的,再次使用的,就可能出现内存泄露。」

「那么如何有效的避免呢?」

「事实上,在 ThreadLocalMap 中的 set/getEntry 方法中,会对 key 为null(也即是 ThreadLocal 为 null)进行判断,如果为 null 的话,那么是会对 value 置为 null 的。我们也可以通过调用 ThreadLocal 的 remove 方法进行释放!」

浏览 37
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报