面试中再问到ThreadLocal,应该这么答

简介: ThreadLocal 提供了线程本地变量的实例,它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本应用场景1.可以使每个线程需要一个独享的对象,每个Thread内有自己的实例副本比如构造一个线程安全的SimpleDateFormat应用场景2.让每个线程内需要保存全局变量例如使用拦截器中获取用户信息,可以让不同方法直接使用,避免参数传递的麻烦在一个线程的生命周期内,都通过这个静态ThreadLocal实例的get()方法取得自己set过的那个对象,避免了将这个对象(例如user对象)作为参数传递的麻烦我在工作中也是使用它来让同一个线程内的不同方法间的全局数据共享对了

「面试中经常被问到ThreadLocal的内容,今天我们来还原下面试现场」

面试官:看你简历写了熟悉JDK常用类源码,那我问问,先来个简单的,你知道ThreadLocal是什么?

「回答:」

ThreadLocal 提供了线程本地变量的实例,它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本

面试官:ok,那你知道它的应用场景,你在工作中有用到它么

「回答:」

应用场景1.可以使每个线程需要一个独享的对象,每个Thread内有自己的实例副本

比如构造一个线程安全的SimpleDateFormat

/**
 * 生产出线程安全的日期格式化工具
 */
class ThreadSafeFormatter {
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}

应用场景2.让每个线程内需要保存全局变量

例如使用拦截器中获取用户信息,可以让不同方法直接使用,避免参数传递的麻烦

class Interceptor {
    public void preHandle(String userId) {
        //模拟查询数据库后获取到的用户信息
        User user = new User("月伴飞鱼 " + userId);
        UserContextHolder.holder.set(user);
        new Service().process();
    }
}
class UserContextHolder {
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
    String name;
    public User(String name) {
        this.name = name;
    }
}

在一个线程的生命周期内,都通过这个静态ThreadLocal实例的get()方法取得自己set过的那个对象,避免了将这个对象(例如user对象)作为参数传递的麻烦

我在工作中也是使用它来让同一个线程内的不同方法间的全局数据共享

对了,Spring中也有使用到ThreadLocal的地方,之前看源码看到过

DateTimeContextHolder类,用到了ThreadLocal

public final class DateTimeContextHolder {
    private static final ThreadLocal<DateTimeContext> dateTimeContextHolder = new NamedThreadLocal("DateTimeContext");

还有RequestContextHolder类

public abstract class RequestContextHolder {
    private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal("Request attributes");
    private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal("Request context");

面试官:好的,不错哦,小伙子那我来点难的,你讲讲ThreadLocal的原理吧

「回答:」

每个线程中都有一个ThreadLocalMap数据结构,当执行set方法时,其值是保存在当前线程的threadLocals变量中,当执行set方法中,是从当前线程的threadLocals变量获取

//java.lang.Thread#threadLocals
ThreadLocal.ThreadLocalMap threadLocals = null;

下面Thread,ThreadLocal以及ThreadLocalMap三者之间的关系


ThreadLocalMap类

也就是ThreadLocal.ThreadLocalMap threadLocals

ThreadLocalMap类是每个线程Thread类里面的变量,里面最重要的是一个键值对数组Entry[] table,可以认为是一个map,键值对:

• 键:这个ThreadLocal

• 值:实际需要的成员变量,比如user或者simpleDateFormat对象

private Entry[] table;

Hash冲突

ThreadLocalMap这里采用的是线性探测法,也就是如果发生冲突,就继续找下一个空位置,而不是用链表拉链,Entry中也没有next字段,所以就不存在链表的情况了

每个ThreadLocal对象都有一个hash值threadLocalHashCode,每初始化一个ThreadLocal对象,hash值就增加一个固定的大小0x61c88647

private static final int HASH_INCREMENT = 0x61c88647;
private final int threadLocalHashCode = nextHashCode();

「在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,过程如下:」

1、如果当前位置是空的,那么正好,就初始化一个Entry对象放在位置i

2、不巧,位置i已经有Entry对象了,如果这个Entry对象的key正好是即将设置的key,那么重新设置Entry中的value

3、很不巧,位置i的Entry对象,和即将设置的key没关系,那么只能找下一个空位置

这样的话,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置

主要方法介绍

「T initialValue():初始化」

protected T initialValue() {
//如果不重写本方法,这个方法会返回null
        return null;
    }
private T setInitialValue() {
//当线程第一次使用get方法访问变量时,将调用此方法
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        //取出map中属于本ThreadLocal的value
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get的时候,才会触发

当线程第一次使用get方法访问变量时,将调用此方法,除非线程先前调用了set方法,在这种情况下,不会为线程调用本initialValue方法

通常,每个线程最多调用一次此方法,但如果已经调用了remove()后,再调用get(),则可以再次调用此方法

如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象。

「void set(T t):为这个线程设置一个新值」和setInitialValue方法很类似

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

「T get():得到这个线程对应的value。如果是首次调用get(),则会调用initialValue来得到这个值」

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        //取出map中属于本ThreadLocal的value
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

get方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value

「void remove():删除对应这个线程的值,代码很简单」

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

面试官:ok,ok,好了,好了,那你知道使用ThreadLocal会有哪些问题么,你是怎么解决的


内存泄漏

内存泄漏是指某个对象不再有用,但是占用的内存却不能被回收

我们知道,如果这个对象只被弱引用关联(没有任何强引用关联),那么这个对象就可以被回收 所以弱引用不会阻止GC,因为这个弱引用的机制,ThreadLocalMap的每个Entry都是一个对key的弱引用,同时每个Entry都包含了一个对value的强引用

正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了

但是,如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收,因为有以下的调用链:Thread ---> ThreadLocalMap ---> Entry(key为null) ---> value

因为value和Thread之间还存在这个强引用链路,所以导致value无法回收,就可能会出现OOM

JDK已经考虑到了这个问题,所以在set,remove,rehash方法中会扫描key为null的Entry,并把对应的value设置为null,这样value对象就可以被回收

但是如果一个ThreadLocal不被使用,那么实际上set,remove,rehash方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就导致了value的内存泄漏

「如何避免内存泄露」

调用remove方法,就会删除对应的Entry对象,可以避兔内存泄漏,所以使用完ThreadLocal之后,应该调用remove方法

ThreadLocal<String> localName = new ThreadLocal();
try {
    localName.set("月伴飞鱼");
    // 其它业务逻辑
} finally {
    localName.remove();
}

如果在Spring中,可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove()方法等,造成内存泄漏

面试官:可以,还有么?

空指针问题

之前遇到过一个空指针问题

总结:ThreadLocal在进行get之前,必须先set,否则会报空指针异常

public class ThreadLocalNPE {
    ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();
    public void set() {
        longThreadLocal.set(Thread.currentThread().getId());
    }
    //拆装箱问题
    public Long get() {//long:NullPointerException
        Long res = longThreadLocal.get();
        return res;
    }
    public static void main(String[] args) {
        ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
        System.out.println(threadLocalNPE.get());//NullPointerException
    }
}

还有就是如果在每个线程中ThreadLocal.set()进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的 ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题

面试官:嗯嗯,这个问题先到这吧

相关文章
|
6月前
|
存储 Java 中间件
《吊打面试官系列》从源码全面解析 ThreadLocal 关键字的来龙去脉
《吊打面试官系列》从源码全面解析 ThreadLocal 关键字的来龙去脉
|
6月前
|
存储 安全 Java
面试题:用过ThreadLocal吗?ThreadLocal是在哪个包下的?看过ThreadLocal源码吗?讲一下ThreadLocal的get和put是怎么实现的?
字节面试题:用过ThreadLocal吗?ThreadLocal是在哪个包下的?看过ThreadLocal源码吗?讲一下ThreadLocal的get和put是怎么实现的?
78 0
|
2月前
|
安全 Java 数据库连接
反问面试官3个ThreadLocal的问题
接下来,我想先说说ThreadLocal的用法和使用场景,然后反问面试官3个关于ThreadLocal的话题。
反问面试官3个ThreadLocal的问题
|
4月前
|
存储 缓存 Java
Java面试题:解释Java中的内存屏障的作用,解释Java中的线程局部变量(ThreadLocal)的作用和使用场景,解释Java中的锁优化,并讨论乐观锁和悲观锁的区别
Java面试题:解释Java中的内存屏障的作用,解释Java中的线程局部变量(ThreadLocal)的作用和使用场景,解释Java中的锁优化,并讨论乐观锁和悲观锁的区别
51 0
|
4月前
|
并行计算 算法 安全
Java面试题:解释Java内存模型的内存屏障,并讨论其对多线程并发的影响,解释Java中的线程局部变量(ThreadLocal)的工作原理,解释Java中的ForkJoinPool的工作原理
Java面试题:解释Java内存模型的内存屏障,并讨论其对多线程并发的影响,解释Java中的线程局部变量(ThreadLocal)的工作原理,解释Java中的ForkJoinPool的工作原理
42 0
|
4月前
|
Java 数据库连接
Java面试题:Java内存模型中的happens-before关系,Java中的ThreadLocal是如何工作的?Java中的CountDownLatch和CyclicBarrier的区别?
Java面试题:Java内存模型中的happens-before关系,Java中的ThreadLocal是如何工作的?Java中的CountDownLatch和CyclicBarrier的区别?
37 0
|
5月前
|
存储 Java 容器
研二学妹面试字节,竟倒在了ThreadLocal上,这是不要应届生还是不要女生啊?
【6月更文挑战第1天】研二学妹面试字节,竟倒在了ThreadLocal上,这是不要应届生还是不要女生啊?
47 5
|
5月前
|
存储 Java 数据库连接
来探讨一下最近面试问的ThreadLocal问题
来探讨一下最近面试问的ThreadLocal问题
|
6月前
|
存储 缓存 安全
面试被问ThreadLocal要怎么回答?
ThreadLocal是Java中为每个线程提供独立变量副本的类,避免多线程同步。它内部维护ThreadLocalMap,存在内存泄漏风险,使用后需调用remove()。常用于记录用户请求数据、事务处理、日志记录和连接池管理等场景。注意内存泄漏和性能影响,以及与InheritableThreadLocal和同步代码块的区别。【5月更文挑战第7天】
|
6月前
|
存储 Java
面试高频 ThreadLocal类详解
面试高频 ThreadLocal类详解
37 0