Java面试必问:ThreadLocal终极篇

ThreadLocal的作用主要是做**数据隔离**,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,防止自己的变量被其它线程篡改。

ThreadLocal的使用场景

事务隔离

Spring的事务主要是ThreadLocalAOP去做实现的。主要是在TransactionSynchronizationManager这个类里面。

Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。

日期隔离

上线后发现部分用户的日期居然不对了,排查下来是SimpleDataFormat的锅,当时我们使用SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。
其实要解决这个问题很简单,让每个线程都new 一个自己的 SimpleDataFormat就好了,但是1000个线程难道new1000个SimpleDataFormat
所以当时我们使用了线程池加上ThreadLocal包装SimpleDataFormat,再调用initialValue让每个线程有一个SimpleDataFormat的副本,从而解决了线程安全的问题,也提高了性能。

避免方法过度传参

在项目中存在一个线程经常遇到横跨若干方法调用,需要传递的对象,也就是上下文(Context),它是一种状态,经常就是是用户身份、任务信息等,就会存在过渡传参的问题。
使用到类似责任链模式,给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,对象参数就传不进去了,所以我使用到了ThreadLocal去做了一下改造,这样只需要在调用前在ThreadLocal中设置参数,其他地方get一下就好了。

cookie & session

很多场景的cookie,session等数据隔离都是通过ThreadLocal去做实现的。

底层实现的原理

  1. 一个线程拥有一个ThreadLocalMap ,可以存储多个ThreadLocal对象。
  2. ThreadLocal对象作为key、独享数据作为value。

    ThreadLocalMap

    1. public void set(T value) {
    2. Thread t = Thread.currentThread();// 获取当前线程
    3. ThreadLocalMap map = getMap(t);// 获取ThreadLocalMap对象
    4. if (map != null) // 校验对象是否为空
    5. map.set(this, value); // 不为空set
    6. else
    7. createMap(t, value); // 为空创建一个map对象
    8. }
    ThreadLocalMap我们需要关注一下,线程Thread有一个类型为ThreadLocalMap的变量:threadLocals
    1. public class Thread implements Runnable {
    2. /* ThreadLocal values pertaining to this thread. This map is maintained
    3. * by the ThreadLocal class. */
    4. ThreadLocal.ThreadLocalMap threadLocals = null;
    5. /*
    6. * InheritableThreadLocal values pertaining to this thread. This map is
    7. * maintained by the InheritableThreadLocal class.
    8. */
    9. ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    10. ThreadLocalMap getMap(Thread t) {
    11. return t.threadLocals;
    12. }
    每个线程Thread都维护了自己的threadLocals变量,每个线程创建ThreadLocal的时候,实际上数据是存入线程的threadLocals变量里面的,从而实现了隔离。
    1. static class ThreadLocalMap {
    2. static class Entry extends WeakReference<ThreadLocal<?>> {
    3. /** The value associated with this ThreadLocal. */
    4. Object value;
    5. Entry(ThreadLocal<?> k, Object v) {
    6. super(k);
    7. value = v;
    8. }
    9. }
    10. ……
    11. }
    ThreadLocalMap并未实现Map接口,而且他的Entry是继承WeakReference(弱引用)的,也没有看到HashMap中的next,所以不存在链表了。

    如何解决Hash冲突?

    ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个threadLocalHashCode,在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,**int i = key.threadLocalHashCode & (len-1)**
    image.png
    在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置,set和get如果冲突严重的话,效率还是很低的。

    ThreadLocal的实例以及其值存放在哪里?

    在Java中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存,而堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。

因为ThreadLocal实例实际上也是被创建的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。

共享线程的ThreadLocal数据

使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。

弱引用问题

image.png
ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。
image.png
弱引用: :::info 只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存。
不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。 ::: 这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。 :::warning ThreadLocalMap的key是ThreadLocal,如果ThreadLocal被回收了,也就是map的key为空了,value就再也无法被访问到了。大量的无法被访问的value就导致内存泄漏了。 :::

每个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回收,就不会出现内存泄露

线程对象不被回收的情况,这就发生了内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露

Java为了最小化减少内存泄露的可能性和影响,在ThreadLocal的get,set的时候都会清除线程Map里所有key为null的value。

如何防止内存泄漏

在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会将value置为null。

也可以通过调用ThreadLocal的remove方法进行释放!

  1. ThreadLocal<String> localName = new ThreadLocal();
  2. try {
  3. localName.set("张三");
  4. ……
  5. } finally {
  6. localName.remove();
  7. }

ThreadLocalMap的key设计成弱引用?

key不设置成弱引用的话就会造成和entry中value一样内存泄漏的场景。

一个线程对应一块工作内存,线程可以存储多个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,那么也将导致内存泄漏。(内存泄露的重点)

FastThreadLocal

ThreadLocal的不足,我觉得可以通过看看netty的fastThreadLocal来弥补