前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >面试专题:深入分析ThreadLocal原理及其应用

面试专题:深入分析ThreadLocal原理及其应用

原创
作者头像
小明爱吃火锅
发布2024-02-07 16:26:09
1840
发布2024-02-07 16:26:09
举报
文章被收录于专栏:小明说Java小明说Java

ThreadLocal的作用

ThreadLocal是Java中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据。为多线程环境下,变量安全提供的一种解决思路,ThreadLocal是给没一个线程建立一个自己的单独的变量副本,每个线程都可以独立的去改变自己的变量副本,从而不会影响其他线程

ThreadLocal源码

get() 用来获取ThreadLocal在当前线程中保存的变量副本

代码语言:java
复制
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 操作ThreadLocalMap
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();
}

set() 用来设置当前线程中的变量副本

代码语言:java
复制
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

remove() 用来移除当前线程中变量的副本。

代码语言:javascript
复制
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

initialValue() 是一个protected方法,一般是用来使用时进行重写。

代码语言:javascript
复制

protected T initialValue() {
    return null;
}

ThreadLocal的原理

通过上述ThreadLocal的源码查看,其内部主要依靠ThreadLocalMap。内部类ThreadLocalMap才是真正实现线程隔离机制的关键,类似Map,由键值对key/value组成的一个Entry数组,key是ThreadLocal本身的一个弱引用,也就是当前ThreadLocal

对象,value就是对应的线程变量副本值。

ThreadLocalMap数据结构,采用数组+开放地址法

代码语言:javascript
复制
static class Entry extends WeakReference<> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}

ThreadLocal注意事项

上述原理可以看到,Entry继承了WeakReference,即Entry的key是弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉(GC)。所以key会在垃圾回收的时候被回收掉, 而key对应的value则不会被回收, 这样会导致一种现象:key为null,value有值

所以,如果ThreadLocal对象越来越多,久而久之,ThreadLocalMap里面就会有很多没有key的value,就会造成内存不足,进而内存泄漏。

解决办法内存泄漏

(1)自动清除,ThreadLocal会自动清除key为null的value

ThreadLocal的get()、set()的时候都会清除线程ThreadLocalMap里所有key为null的value。这也是ThreadLocal的巧妙,防止程序忘记手动remove。

(2)手动清除,使用完毕后及时调用ThreadLocal.remove()

remove方法会主动将当前的key和value(Entry)进行清除。

(3)把ThreadLocal设置为全局变量

ThreadLocal设置为全局变量使得它无法被GC回收。因为成员变量中使用就将修饰符设置为public static,那么内存中一直存在这个key,所以value也就不会存在key为null的情况, 也就不会内存泄漏。

模拟ThreadLocal内存泄露案例

设置程序堆大小

接下来用代码模拟ThreadLocal内存泄露的问题,并查看有没有手动回收的情况,有什么不一样。案例代码如下,也就是,创建30个对象,每个大对象,占用 10 m。

代码语言:java
复制
public class ThreadLocalOOM {

    // 全局ThreadLocal变量
    public static ThreadLocal<LargeObject> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(30);
        // 提交大量任务到线程池
        for (int i = 0; i < 30; i++) {
            executor.submit(new Runnable() {
                @Override
                public void run() {
                    // 在每个任务中分配一个大对象,并存入ThreadLocal
                    LargeObject largeObject = new LargeObject();
                    threadLocal.set(largeObject);
                }
            });
        }
        // 关闭线程池,但这并不会自动清理线程局部变量
        executor.shutdown()
    }

    static class LargeObject {
        // 假设这是一个非常大的对象,占用大量内存
        byte[] data = new byte[1024 * 1024 * 10]; // 10MB的数据数组
    }
}

没有手动remove

调用remove,不会出现内存泄漏

那么为什么要这样设计呢?

问题1:Entry的key可以设置为强引用吗?

若是强引用,即使线程变为null,key的引用依然指向ThreadLocal对象,GC也不会回收,导致这一块内存一直存在,除非该线程结束,也就是只能手动删除,否则这对于程序来说,就出现了内存泄漏。

问题2:Entry的value可以设置为弱引用吗?

假如value被设计成弱引用,那么很有可能当你需要取这个value值的时候,取出来的值是一个null。因为一旦把本应该强引用的value设计成了弱引用,那么只要jvm执行一次GC操作,value就直接被回收掉了。当你需要从当前线程中取值的时候,最终得到的就是null。

使用场景

(1)全局存储用户信息,服务端拦截用户请求,将用户的验证信息保存到ThreadLocal中,使得整个线程上下文都可以获取,就不用每次去获取用户信息。

(2)多线程进行日期格式化

由于SimpleDateFormat是线程不安全的,导致多个线程共享这个同一个对象时出现线程不安全问题,所以多线程访问可能会报错。

代码语言:java
复制
public class DateUtilNotSale {

    private static final ThreadLocal<SimpleDateFormat> sdf = ThreadLocal.withInitial(
            () -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
    );

    public static Date parse(String dateStr){
        Date date = null;
        try {
        //    date = sdf.parse(dateStr); parse线程不安全,没有做线程安全处理,不能保证原子性,
            //    导致多个线程共享这个同一个对象时出现线程不安全问题
            date = sdf.get().parse(dateStr);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }
}

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ThreadLocal的作用
  • ThreadLocal源码
  • ThreadLocal的原理
  • ThreadLocal注意事项
    • 解决办法内存泄漏
      • 模拟ThreadLocal内存泄露案例
      • 使用场景
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
      http://www.vxiaotou.com