深入理解ThreadLocal

本文深入探讨了Java中的ThreadLocal,包括其概念、应用场景、内部原理和内存泄露问题。ThreadLocal提供了一种线程安全的数据存储方式,常用于全局用户信息存储、日期处理和日志追踪ID管理。通过弱引用设计,ThreadLocal可以防止内存泄露,但需要适时调用remove方法。此外,文章还介绍了如何在父子线程间共享数据,推荐使用InheritableThreadLocal。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

​​​​​​

ThreadLocal是什么

ThreadLocal的应用场景

ThreadLocal的内部原理

ThreadLocal内存泄露问题

父子线程如何共享数据

 

 

ThreadLocal是什么

java.lang.ThreadLocal是JDK中提供的一个类,在并发编程中,为解决线程安全问题提供了一种用空间换时间的新思路。

 

在一些高并发的场景中,如果需要对公共资源进行操作,我们第一时间就会想到使用synchronized或Lock,给访问公共资源的代码上锁,来保证了代码的原子性。但是多个线程同时竞争同一把锁的时候,可能会造成大量的锁等待,可能会浪费很多时间,让系统的响应时间变慢。这个时候我们就可以考虑是否可以使用ThreadLocal。

 

将类变量放到ThreadLocal类型的对象中,就可以使变量在每个线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象。

ThreadLocal的应用场景

1、全局存储用户信息

      当用户登陆后,会将用户信息存入token中并返回给前端,当用户调用需要授权的接口时,前端会把token放到header中去请求后端接口,后端在拦截器中解析token,获取用户信息,然后存放到某个工具类的ThreadLocal变量中,后续执行代码过程中,就不需要关注如何获取用户信息,只需要使用工具类get方法就可以获取。

 

2、SimpleDateFormat与ThreadLocal结合使用

      大家都知道SimpleDateFormat是不安全类,但是做一些日期处理的时候又经常会用到这个类,这个时候我们可以与ThreadLocal结合进行使用,从而避免了线程安全问题。

// 这样来申明全局变量
public final static ThreadLocal<SimpleDateFormat> DF = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

3、org.slf4j.MDC处理链路追踪ID

     MDC类本身包含了一个MDCAdapter对象,此对象里又包含了一个ThreadLocal的全局变量,可以用来处理日志相关的信息。例如常见的我们处理traceId的时候,先在过滤器里生成traceId并放入MDC,然后在日志文件里配置traceId,这样我们打印日志的时候就可以看到traceId了。

@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
    // 此处生成traceId...

    // 将traceId放入MDC
    MDC.put("traceId", traceId);
    
    try {
        filterChain.doFilter(servletRequest, servletResponse);
    } catch (Exception e) {
        MDC.remove("traceId");
    }
}
<!-- 控制台日志 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
        <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %level %C.%M[%F:%L] traceId[%X{traceId:-default}] - %msg%n
        </Pattern>
    </encoder>
</appender>

ThreadLocal的内部原理

想要搞清楚ThreadLocal的底层实现原理,我们不得不扒一下jdk源码。先看一下ThreadLocal类,发现有set、get、remove三个关键的方法:

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程的成员变量ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    //如果map不为空
    if (map != null)
        //将值设置到map中,key是this,即threadLocal对象,value是传入的value值
        map.set(this, value);
    else
       //如果map为空,则需要创建新的map对象
        createMap(t, value);
}
public T get() {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程的成员变量ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //根据threadLocal对象从map中获取Entry对象
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            //获取保存的数据
            T result = (T)e.value;
            return result;
        }
    }
    //初始化数据
    return setInitialValue();
}
public void remove() {
    //获取当前线程的ThreadLocalMap对象
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        //如果ThreadLocalMap对象不为空,则删除,key是this,即threadLocal对象
        m.remove(this);
}

从以上源码中,可以看到set、get、remove方法实际上都是在操作ThreadLocalMap对象中的数据,而ThreadLocalMap对象又是从当前线程中获取到的,所以不同的线程之间,ThreadLocalMap对象中的数据是隔离开的。

7b9121cab4cd4e9cac2947b20e2028b0.png

 

ThreadLocal内存泄露问题

首先一句话简单概括什么是内存泄露:由于某种原因,导致程序中的某些资源(内存)无法得到释放,因此造成了系统内存泄露。

 

在真正解释内存泄露之前我们还得简单回顾一下JAVA中的四种引用类型的区别:

1、强引用:发生了GC,对象也不会被回收

2、软引用(SoftReference):发生了GC,如果内存足够,则对象不会被回收;反之,会被回收

3、弱引用(WeakReference):发生了GC,无论内存是否足够,对象都会被回收

4、虚引用(PhantomReference):所指向的对象获取不到、拿出来是null,因此也叫幽灵对象

 先来看下ThreadLocalMap的结构

static class ThreadLocalMap {
	//此处省略...

    //可以看出Entry继承了弱引用
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

    //此处省略...
}

那么问题来了,ThreadLocalMap的Entry为什么使用了弱引用,跟内存泄露又有什么关系呢?

我们分别来看假设key是强引用原本的弱引用情况下怎么样会发生内存泄漏

e49171a0dd7740c38482f1f0aa3573a3.png

 8e799c3a83434a95a5c5e23799e7cff1.png

从上面的对比,我们可以看到,ThreadLocal 内存泄漏的根源是:由于ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除(remove()方法)对应 key 就会导致内存泄漏。要避免内存泄漏有两种方式:

1、使用完ThreadLocal,调用其remove方法删除对应的Entry

2、使用完ThreadLocal,当前Thread也随之运行结束

第一种方式很好理解。但是第二种方式显然就不好控制,特别是使用线程池的时候,线程执行代码结束后是不会销毁的。也就是说,只要记得在使用完ThreadLocal后及时的调用 remove,无论 key 是强引用还是弱引用都不会有问题。

那么为什么key要用弱引用呢?事实上,在ThreadLocalMap中,只要调用了它的get、set或remove三个方法中的任何一个方法,都会自动触发清理机制,将key为null的value值清空,如果key和value都是null,那么Entry对象会被GC回收。如果所有的Entry对象都被回收了,ThreadLocalMap也会被回收了,这样就能最大程度(就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收.对应value在下一次ThreadLocaIMap调用 set/get/remove中的任一方法的时候会被清除,从而避免内存泄漏)的解决内存泄露问题。

// 这里是清除过期的entry的核心方法
private int expungeStaleEntry(int staleSlot) {
    //此处省略...

    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
        	// 当k为null时,把value也改为null
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            //此处省略...
        }
    }
    return i;
}

父子线程如何共享数据

前面介绍的ThreadLocal都是在一个线程中保存和获取数据的。但在实际工作中,有可能是在父子线程中共享数据的。即在父线程中往ThreadLocal设置了值,在子线程中能够获取到。

public class ThreadLocalTest {

    public static void main(String[] args) {
        ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
        threadLocal.set(6);
        System.out.println("父线程获取数据:" + threadLocal.get());

        new Thread(() -> {
            System.out.println("子线程获取数据:" + threadLocal.get());
        }).start();
    }
}

执行结果:

父线程获取数据:6
子线程获取数据:null

你会发现,在这种情况下使用ThreadLocal是行不通的。main方法是在主线程中执行的,相当于父线程。在main方法中开启了另外一个线程,相当于子线程。显然通过ThreadLocal,无法在父子线程中共享数据。那么这个时候可以使用InheritableThreadLocal,它是JDK自带的类,继承了ThreadLocal类。

修改代码之后:

public class ThreadLocalTest {

    public static void main(String[] args) {
        InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
        threadLocal.set(6);
        System.out.println("父线程获取数据:" + threadLocal.get());

        new Thread(() -> {
            System.out.println("子线程获取数据:" + threadLocal.get());
        }).start();
    }
}

执行结果:

父线程获取数据:6
子线程获取数据:6

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值