TheadLocal:当前线程的全局变量

        新需求业务场景上需要新增2个字段,接口新增参数意味着很多类和方法的逻辑都需要改变,需要先判断是否属于该业务场景,再做对应的逻辑。原本的打算是在入口处新增变量,在操作数据的时候进行逻辑判断将变量进行存储或查询。

        若全链路都变更入参和结构,很明显代码上费时费力,若后续还要增加业务场景,又需要再改一遍。如果有一个方法可以传递全局变量,而且仅限于当前线程就好了。

        到此,会想到有两种解决方案:之前用的比较少的ThreadLocal或者使用redis缓存。考虑到新增字段都是些增删改查的操作,没有必要存到redis中,故使用ThreadLocal。

ThreadLocal的定义

    ThreadLocal是 Java 中的一个类,它提供了一种线程封闭的机制,用于在多线程环境下实现线程级别的变量副本,并确保每个线程都可以独立访问和修改自己的副本,而不会影响其他线程的副本。

public class ThreadLocal {
    public T get() {
        // 返回当前线程关联的变量副本
    }

    public void set(T value) {
        // 设置当前线程关联的变量副本为指定值
    }

    public void remove() {
        // 移除当前线程关联的变量副本
    }
    
    // 其他方法...
}

    ThreadLocal是一个泛型类,通过类型参数 T 来指定变量的类型。它提供了 get()set()remove() 方法来操作当前线程关联的变量副本。

  • get() 方法用于获取当前线程关联的变量副本。
  • set() 方法用于设置当前线程关联的变量副本为指定的值。
  • remove() 方法用于移除当前线程关联的变量副本。

        使用 ThreadLocal,每个线程都可以独立访问和修改自己的变量副本,而不会受到其他线程的干扰。这种机制对于保持线程安全和避免共享变量的竞争非常有用。

        ThreadLocal有两个特性:每个Thread的变量只能由当前Thread使用;由于其他线程不可访问,则不存在多线程间共享的问题。 

需要注意的是,每个线程只能访问自己关联的变量副本,而无法直接访问其他线程的副本。因此,在使用 ThreadLocal时要特别小心,确保在正确的线程上下文中使用和清理相关的变量副本,以防止数据泄漏或错误使用。

ThreadLocal的使用场景

  1. 线程上下文信息传递:在多线程环境中,有些情况下需要在线程之间传递上下文信息,如用户身份认证信息、语言环境、数据库连接等。通过将这些信息存储在  ThreadLocal 中,可以保证每个线程独立拥有自己的信息副本,从而避免了线程之间的共享和竞争,提高了代码的可维护性和可靠性。

  2. 事务管理:在需要使用事务的应用程序中,可以使用  ThreadLocal 来存储当前线程的事务上下文,确保每个线程都可以独立管理自己的事务,而不会相互干扰。

  3. 解决线程安全问题:SimpleDateFormat 是非线程安全的,如果多个线程共享同一个 SimpleDateFormat 实例,可能会导致日期解析和格式化错误。通过将 SimpleDateFormat 存储在  ThreadLocal 中,每个线程都可以拥有自己的日期格式实例,从而避免了线程安全问题。在Spring项目中Dao层中装配的Connection肯定是线程安全的,其解决方案就是采用ThreadLocal方法,当每个请求线程使用Connection的时候, 都会从ThreadLocal获取一次,如果为null,说明没有进行过数据库连接,连接后存入ThreadLocal中,如此一来,每一个请求线程都保存有一份自己的Connection。于是便解决了线程安全问题。

  4. 缓存管理:在需要对数据进行缓存的场景下,可代替参数的显式传递,使用 ThreadLocal 存储线程本地的缓存对象,每个线程都有自己独立的缓存,可以提高缓存的效率,避免线程间的竞争和同步开销。

 ThreadLocal的优点和缺点

优点:

  1. 线程隔离性: ThreadLocal提供了线程级别的变量副本,每个线程都可以独立访问和修改自己的副本,从而避免了线程间的共享和竞争。这提供了更好的线程安全性和数据隔离性。
  2. 线程安全:由于每个线程独立拥有自己的变量副本,并且无法直接访问其他线程的副本,因此不存在线程安全问题。
  3. 性能提升:在多线程环境下,通过使用ThreadLocal可以避免同步机制的开销,提高程序性能。

缺点:

  1. 内存泄漏风险:由于ThreadLocal使用的是弱引用,如果没有及时清理ThreadLocal的引用,可能会导致垃圾对象无法被回收,从而引发内存泄漏的风险。因此,在使用完毕后,需要手动调用 remove() 方法来清除当前线程关联的变量副本。
  2. 上下文传递困难:由于每个线程只能访问自己关联的变量副本,线程之间的上下文传递变得困难。如果需要在线程间传递数据或上下文信息,需要显式地通过参数传递或其他方式进行。

ThreadLocal的底层实现

        ThreadLocal最朴素的内部实现是Map,这是一个HashMap,又称为ThreadLocalMap。但Java源码并不是Map的实现。这是因为如果多个线程访问同一个map,这个map需要是线程安全的,构造比较麻烦。Java采用了更简单粗暴的做法:每个线程都有自己的ThreadLocal专属map,里面可以存放多个ThreadLocal变量,这样就解决了多线程同时操作一个map带来的多线程并发问题。

        因为要把ThreadLocal的变量当做全局变量使用,需要把变量与初始化函数写在通用的类中,如DDD领域模型中写在Common模块。

public class ThreadLocalUtil {
 
   private static ThreadLocal THREAD_LOCAL = new ThreadLocal<>();


   public static Integer getScene() {
       return THREAD_LOCAL.get();
   }

   public static void initScene(Integer scene) {
       if (THREAD_LOCAL == null) {
           THREAD_LOCAL = new ThreadLocal<>();
       }
       THREAD_LOCAL.set(scene);
   }

   public static void remove() {
       THREAD_LOCAL.remove();
   }
}

ThreadLocal的致命点

上面提到了的ThreadLocal会带来内存泄露的问题,深入分析下:

        一个ThreadLocal实例对应当前线程的一个对象实例,如果把ThreadLocal声明为某个类的实例变量不是静态变量,那么每次创建一个该类的实例就会导致一个新的对象实例被创建。而这些被创建的实例是同一个类的实例,于是同一个线程可能会访问到同一个类的不同实例,这即使不会导致错误,也会导致重复创建同样的对象。如果使用static修饰后,只要相应的类没有被垃圾回收掉,那么这个类就会持有相对应的ThreadLocal实例引用。

TheadLocal:当前线程的全局变量_第1张图片

        ThreadLocal自身并不存储值,而是作为一个key来让线程从ThreadLocal中获取value。        

        ThreadLocalMap中的key是弱引用,所以jvm在垃圾回收时如果外部没有强引用来引用它,ThreadLocal必然会被回收。但是,作为ThreadLocalMap中的key,ThreadLocal被回收后,ThreadLocalMap就会存在null,但value却不为null。如果当前线程一直不结束或者线程结束后不被你销毁,这会产生内存泄露(已分配空间的堆内存由于某种原因未释放或无法释放导致系统内存浪费或程序运行变慢甚至系统奔溃)。

        因此,key弱引用并不是导致内存泄露的原因,而是因为ThreadLocalMap的生命周期与当前线程一样长,并且没有手动删除对应的value

        解决的方法也很简单,只需要打破引用路径中的ThreadLocalMap对对象实例的引用即可。也就是在使用完ThreadLocal之后,必须调用ThreadLocal.remove()。

更多消息资讯,请访问昂焱数据(https://www.ayshuju.com)

你可能感兴趣的:(java,开发语言)