还能这样读ThreadLocal?

2024-01-19 10:52:33 浏览数 (2)

前言

ThreadLocal是Java中的一个类,用于创建线程本地变量,即每个线程都有自己的变量副本,互不干扰。


一、Threadlocal通俗理解

1.1定义

它能够提供了线程的局部变量,让每个线程都可以通过set/get来对这个局部变量进行操作。 不会和其他线程的局部变量进行冲突,实现了线程的数据隔离。

1.2场景

1.2.1日期时间格式化工具类

格式化/转化的实现是用的SimpleDateFormat。

但众所周知SimpleDateFormat不是线程安全的,所以我们就用ThreadLocal来让每个线程装载着自己的SimpleDateFormat对象,以达到在格式化时间时,线程安全的目的。

在方法上创建SimpleDateFormat对象也没问题,但每调用一次就创建一次有点不优雅。

1.2.2Spring事务

Spring提供了事务相关的操作,而我们知道事务是得保证一组操作同时成功或失败的。 这意味着我们一次事务的所有操作需要在同一个数据库连接上。 但是在我们日常写代码的时候是不需要关注这点的。

Spring就是用的ThreadLocal来实现,ThreadLocal存储的类型是一个Map 。 Map中的key 是DataSource,value 是Connection (为了应对多数据源的情况所以是一个Map)。 用了ThreadLocal保证了同一个线程获取一个Connection对象,从而保证一次事务的所有操作需要在同一个数据库连接上。

1.3原理

ThreadLocal是一个壳子,真正的存储结构是ThreadLocal里有ThreadLocalMap这么个内部类 而有趣的是,ThreadLocalMap的引用是在Thread上定义的。

ThreadLocal本身并不存储值,它只是作为key来让线程从ThreadLocalMap获取value。 所以,得出的结论就是ThreadLocalMap该结构本身就在Thread下定义,而ThreadLocal只是作为key,存储set到ThreadLocalMap的变量当然是线程私有的咯。

疑问:

我可以在ThreadLocal下定义Map,key是Thread,value是set进去的值吗? 就是说,为啥我要把ThreadLocal做为key,而不是Thread做为key?这样不是更清晰吗?

答案:

实际上就是所有的线程都访问ThreadLocal的Map,而key是当前线程。 但这有点小问题,一个线程是可以拥有多个私有变量的嘛,那key如果是当前线程的话,意味着还点做点 [手脚],来唯一标识set进去的value 。

还有个问题就是:并发量足够大时,意味着所有的线程都去操作同一个Map,Map体积有可能会膨胀,导致访问性能的下降。 并且这个Map维护着所有的线程的私有变量,意味着你不知道什么时候可以[销毁]。

现在JDK实现的结构就不一样了。 线程需要多个私有变量,那有多个ThreadLocal对象足以,对应的Map体积不会太大。

只要线程销毁了,ThreadLocalMap也会被销毁品。


二、内存泄漏

在使用ThreadLocal时,如果没有及时清理ThreadLocal变量,就会导致内存泄漏问题。

讲内存泄漏问题前,先科普一下Java中有四种引用类型,分别为:

  1. 强引用 (Strong Reference):被强引用关联的对象不会被垃圾回收器回收,只有在该引用被显式地赋值为null后,对象才会被回收。
  2. 软引用 (Soft Reference):被软引用关联的对象只有在内存不足时才会被垃圾回收器回收。
  3. 弱引用 (Weak Reference):被弱引用关联的对象在下一次垃圾回收时一定会被回收。
  4. 虚引用 (Phantom Reference):被虚引用关联的对象在任何时候都可能被垃圾回收器回收。虚引用通常用于跟踪对象被垃圾回收的状态,可以在对象被回收时收到一个系统通知。

ThreadLocal的内部实现是将每个线程维护的变量存储在一个Map中,线程的ThreadLocal变量作为Map的key,而变量值则作为Map的value。当线程结束时,ThreadLocal变量不会被垃圾回收器回收,因为它们仍然被Map所引用。

如果不主动清理ThreadLocal变量,就会导致Map中的所有对象都无法被垃圾回收,从而引起内存泄漏。一般来说,需要在ThreadLocal使用完毕后及时地调用remove方法将其删除。

以下是一个示例代码,演示了如何使用ThreadLocal解决线程安全问题,并展示了如何避免内存泄漏:

代码语言:javascript复制
public class MyService {
    private static ThreadLocal<Object> threadLocal = new ThreadLocal<>();

    public void setValue(Object value) {
        threadLocal.set(value);
    }

    public Object getValue() {
        return threadLocal.get();
    }

    public void removeValue() {
        threadLocal.remove();
    }
}

在使用ThreadLocal时,需要注意以下几点:

  1. 尽量避免创建过多的ThreadLocal变量,因为每个ThreadLocal都需要占用一定的内存。
  2. 在ThreadLocal使用完毕后,必须调用remove方法将其删除,否则会引起内存泄漏。
  3. 在使用ThreadLocal时,建议使用try-finally语句块,确保即使出现异常,也能够及时地清理ThreadLocal变量,避免内存泄漏问题。

疑问1:

为什么要将ThreadLocalMao的key设置为弱引用呢?强引用不香吗?

回答:

外界是通过ThreadLocal来对ThreadLocalMap进行操作的,假设外界使用ThreadLocal的对象被置null了,那ThreadLocalMap的强引用指向ThreadLocal也毫无意义啊。

弱引用反而可以预防大多数内存泄漏的情况。 毕竟被回收后,下一次调用set/get/remove时ThreadLocal内部会清除掉。

疑问2:

建议把ThreadLocal修饰为static,为什么?

回答:

ThreadLocal能实现了线程的数据隔离不在于它自己本身,而在于Thread的Thr eadLocalMap。

所以,ThreadLocal可以只初始化一次只分配一块存储空间就足以了,没必要作为成员变量多次被初始化

0 人点赞