Java 面试高频 ThreadLocal

2023年 8月 9日 90.0k 0

面试题

  • ThreadLocal中ThreadLocalMap的数据结构和关系?
  • ThreadLocal的key是弱引用,这是为什么?
  • ThreadLocal内存泄露问题你知道吗?
  • ThreadLocal中最后为什么要加remove方法?

是什么? 能干嘛

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

主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题。

一句话如何才能不争抢

  • 加入synchronized或者Lock控制资源的访问顺序
  • 人手一份,大家各自安好,没必要抢夺
  • 举个栗子-阿里规范

    为什么SimpleDateFormat是线程不安全的?

    官方文档说明

    测试

    public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        /**
         * 模拟并发环境下使用SimpleDateFormat的parse方法将字符串转换成Date对象
         * @param stringDate
         * @return
         * @throws Exception
         */
        public static Date parseDate(String stringDate)throws Exception {
            return sdf.parse(stringDate);
        }
    
        public static void main(String[] args) throws Exception {
            for (int i = 1; i  {
                    try {
                        System.out.println(DateUtils.parseDate("2020-11-11 11:11:11"));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                },String.valueOf(i)).start();
            }
        }

    源码分析

    SimpleDateFormat类内部有一个Calendar对象引用,它用来储存和这个SimpleDateFormat相关的日期信息。

    例如sdf.parse(dateStr),sdf.format(date) 诸如此类的方法参数传入的日期相关String,Date等等, 都是交由Calendar引用来储存的。这样就会导致一个问题:如果你的SimpleDateFormat是个static的, 那么多个thread 之间就会共享这个SimpleDateFormat, 同时也是共享这个Calendar引用。

    SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。

    举个例子:

    假设线程 A 刚执行完 calendar.setTime(date) 语句,把时间设置为 2020-09-01,但线程还没执行完,线程 B 又执行了 calendar.setTime(date) 语句,把时间设置为 2020-09-02,这个时候就出现幻读了,线程 A 继续执行下去的时候,拿到的 calendar.getTime 得到的时间就是线程B改过之后的。

    如何解决

    • 解决1

    将SimpleDateFormat定义成局部变量。

    缺点:每调用一次方法就会创建一个SimpleDateFormat对象,方法结束又要作为垃圾回收。

    public static void main(String[] args) throws Exception {
            for (int i = 1; i  {
                    try {
                        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                        //System.out.println(DateUtils.parseDate("2020-11-11 11:11:11"));
                        System.out.println(sdf.parse("2020-11-11 11:11:11"));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                },String.valueOf(i)).start();
            }
        }
    • 解决2

    ThreadLocal,也叫做线程本地变量或者线程本地存储

    private static final ThreadLocal  sdf_threadLocal =
                ThreadLocal.withInitial(()-> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    
        /**
         * ThreadLocal可以确保每个线程都可以得到各自单独的一个SimpleDateFormat的对象,那么自然也就不存在竞争问题了。
         * @param stringDate
         * @return
         * @throws Exception
         */
        public static Date parseDateTL(String stringDate)throws Exception {
            return sdf_threadLocal.get().parse(stringDate);
        }
    
        public static void main(String[] args) throws Exception {
            for (int i = 1; i  {
                    try {
                        System.out.println(DateUtils.parseDateTL("2020-11-11 11:11:11"));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                },String.valueOf(i)).start();
            }
        }
    • 其它

    加锁

    第3方时间库

    ThreadLocal Thread ThreadLocalMap 之间的关系

    ThreadLocal :每个线程通过此对象都会返回各自的值,互不干扰,这是因为每个线程都存着自己的一份副本。需要注意的是线程结束后,它所保存的所有副本都将进行垃圾回收(除非存在对这些副本的其他引用)

    ThreadLocal的get操作是这样执行的:ThreadLocalMap map = thread.threadLocals -> return map.getEntry(threadLocal)ThreadLocal的set操作是这样执行的:ThreadLocalMap map = thread.threadLocals -> map.set(threadLocal, value)

    三者的关系是:

    • 每个Thread对应的所有ThreadLocal副本都存放在ThreadLocalMap对象中,key是ThreadLocal,value是副本数据。
    • ThreadLocalMap对象存放在Thread对象中。
    • 通过ThreadLocal获取副本数据时,实际是通过访问Thread来获取ThreadLocalMap,再通过ThreadLocalMap获取副本数据。

    ThreadLocal内存泄露问题

    阿里手册

    什么是内存泄漏

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

    ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。

    强引用、软引用、虚引用、弱引用

    强引用

    当内存不足,jvm开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收。这也是Java中最常见的普通对象的引用,只要还有强引用指向这个对象,就不会被垃圾回收。当这个对象没有了其他的引用关系,只要是超过了引用的作用域,或者显示的将强引用赋值为null,一般就可以进行垃圾回收了。

    软引用

    软引用是相对强引用弱化了一些的引用,对于软引用的对象来说:

    • 当内存充足时,它不会被回收。
    • 当内存不足时。会被回收。

    通常用在对内存敏感的程序中,就像高速缓存。

    弱引用

    发现即回收

    弱引用也是用来描述那些非必需对象,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。在系统GC时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉只被弱引用关联的对象。

    但是,由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间。弱引用和软引用一样,在构造弱引用时,也可以指定一个引用队列,当弱引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况。软引用、弱引用都非常适合来保存那些可有可无的缓存数据。如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用。在JDK1.2版之后提供了WeakReference类来实现弱引用

    // 声明强引用
    Object obj = new Object();
    // 创建一个弱引用
    WeakReference sf = new WeakReference(obj);
    obj = null; //销毁强引用,这是必须的,不然会存在强引用和弱引用

    虚引用

    也称为“幽灵引用”或者“幻影引用”,是所有引用类型中最弱的一个

    一个对象是否有虚引用的存在,完全不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时都可能被垃圾回收器回收。它不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的get()方法取得对象时,总是null。为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知。

    虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录。在JDK1.2版之后提供了PhantomReference类来实现虚引用。

    // 声明强引用
    Object obj = new Object();
    // 声明引用队列
    ReferenceQueue phantomQueue = new ReferenceQueue();
    // 声明虚引用(还需要传入引用队列)
    PhantomReference sf = new PhantomReference(obj, phantomQueue);
    obj = null;

    内存泄漏问题

    回到ThreadLocal这边来说:ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

    就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。

    按照道理一个线程使用完,ThreadLocalMap是应该要被清空的,但是现在线程被复用了。

    那怎么解决?

    在代码的最后使用remove就好了,我们只要记得在使用的最后用remove把值清空就好了。

    ThreadLocal localName = new ThreadLocal();
    try {
        localName.set("张三");
        ……
    } finally {
        localName.remove();
    }

    remove的源码很简单,找到对应的值全部置空,这样在垃圾回收器回收的时候,会自动把他们回收掉。

    那为什么ThreadLocalMap的key要设计成弱引用?

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

    补充一点:ThreadLocal的不足,我觉得可以通过看看netty的fastThreadLocal来弥补,大家有兴趣可以康康。

    key为null的entry的值传递的bug

    ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如正好用在线程池),这些key为null的Entry的value就会一直存在一条强引用链。

    虽然弱引用,保证了key指向的ThreadLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value,因此弱引用不能100%保证内存不泄露。我们要在不使用某个ThreadLocal对象后,手动调用remoev方法来删除它,

    另外在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。--值传递的问题

    值传递的问题

    ThreadLocal 无法共享线程中的数据,InheritableThreadLocal 可以再父子线程之间完成值的传递

    InheritableThreadLocal中createMap,以及getMap方法处理的对象不一样了,其中在ThreadLocal中处理的是threadLocals,而InheritableThreadLocal中的是inheritableThreadLocals。Inheritablethreadlocal在父子线程传递的时候,依托的是thread的,在执行init( )初始化方法。在init方法中会执行parent.inheritableThreadLocals,这个方法会把父线程的值赋给子线程,如果我们使用的是线程池,这样不会再重新执行init()初始化方法,而是直接使用已经创建过的线程,所以这里的值不会二次产生变化,做不到真正的父子线程数据传递,就会出现数据错乱的问题。

    TransmittableThreadLocal 的原理:它底层使用的TtlRunnable, 在TtlRunnable 构造方法中,会获取当前线程中所有的上下文,并储存在 AtomicReference 中。而且TtlRunnable 是实现于 Runnable,在 TtlRunnable run 方法中会执行 Runnable run 方法。当线程执行时,TtlRunnable 会从 AtomicReference 中获取出调用线程中所有的上下文,并把上下文通过 TransmittableThreadLocal.Transmitter.replay 方法把上下文复制到当前线程。这样就可以在使用线程池的情况下也可以完成值的传递了。

    ThreadLocalMap的原理

    介绍

    ThreadLocalMap 是一个自定义map,它并没有实现Map接口,而且他的Entry是继承WeakReference(弱引用)的,也没有看到HashMap中的next,所以不存在链表了。

    为什么底层用数组?没有了链表怎么解决Hash冲突呢?

    用数组是因为,我们开发过程中可以一个线程可以有多个TreadLocal来存放不同类型的对象的,但是他们都将放到你当前线程的ThreadLocalMap里,所以肯定要数组来存。

    从源码里面看到ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个threadLocalHashCode,在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,

    int i = key.threadLocalHashCode & (len-1)。然后会判断一下:如果当前位置是空的,就初始化一个Entry对象放在位置i上;如果位置i不为空,如果这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value;如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止。

    小总结

    • ThreadLocal 并不解决线程间共享数据的问题
    • ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
    • ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
    • 每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题
    • ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论