ThreadLocal

内容纲要

使用场景

  • 场景1:每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random
  • 场景2:每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦
    • Spring Security获取到Authentication用户信息时也采用到ThreadLocal实现
    • SecurityContextHolder类中有私有的静态变量SecurityContextHolderStrategy strategy,其实现类为ThreadLocalSecurityContextHolderStrategy,其中实现类中则创建了一个ThreadLocal的对象来存储对应线程的SecurityContext即用户信息
    • 源码展示
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {

   private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();

   @Override
   public void clearContext() {
      contextHolder.remove();
   }

   @Override
   public SecurityContext getContext() {
      //获取到ThreadLocal中对应的用户信息
      SecurityContext ctx = contextHolder.get();
      if (ctx == null) {
         ctx = createEmptyContext();
         contextHolder.set(ctx);
      }
      return ctx;
   }

   @Override
   public void setContext(SecurityContext context) {
      Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
      contextHolder.set(context);
   }

   @Override
   public SecurityContext createEmptyContext() {
      return new SecurityContextImpl();
   }

}
  • 总结
    1. 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象)
    2. 在任何方法中都可以轻松获取到该对象

好处

  • 达到线程安全
  • 不需要加锁,提高执行效率
  • 更有效的利用内存,节省开销:相比每个任务都新建一个实例,显然使用ThreadLocal可以节省内存和开销
  • 免去传参的繁琐:无论是场景一的工具类,还是场景二的用户名,都可以在任何地方直接通过ThreadLocal拿到,再也不需要每次都传同样的参数。ThreadLocal使得代码耦合度更低更优雅imooc

详解

主要方法介绍

  • T initialValue(): 初始化
    1. 该方法会返回当前线程对应的“初始值”这是一个延迟加载的方法,只有在调用get的时候,才会触发
    2. 当线程第一次使用get方法访问变量时,将调用此方法,除非线程先前调用了set方法,在这种情况下,不会为线程调用本initialValue方法
    3. 通常,每个线程最多调用一次此方法,但如果已经调用了remove0后,再调用get0,则可以再次调用此方法
    4. 如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来重写initialValue() 方法,以便在后续使用中可以初始化副本对象
  • void set(T t): 为这个线程设置一个新值
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = this.getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        this.createMap(t, value);
    }
}
  • T get():得到这个线程对应的value。如果是首次调用get0,则会调用initialize来得到这个值
    • get方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = this.getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            T result = e.value;
            return result;
        }
    }
    return this.setInitialValue();
}
  • void remove(): 删除对应这个线程的值
public void remove() {
    ThreadLocalMap m = this.getMap(Thread.currentThread());
    if (m != null) {
        m.remove(this);
    }
}

ThreadLocalMap 类

  • ThreadLocalMap 类,也就是Thread.threadLocals
  • ThreadLocalMap 类是每个线程Thread类里面的变量,里面最重要的是一个键值对数组Entryll table,可以认为是一map,键值对
    • 键:这个ThreadLoca
    • 值:实际需要的成员变量,比如user或者simpleDateFormat对象

冲突: HashMap

  • Java8 HashMap 结构
  • ThreadLocalMap这里采用的是线性探测法,也就是如果发生冲突,就继续找下一个空位置,而不是用链表拉链

注意点

  • 内存泄漏
  • ThreadLocalMap 的每个 Entry 都是一个对key的弱引用,同时每个Entry 都包含了一个对value的强引用
  • 正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了
  • 但是,如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收,因为有以下的调用链
  • 因为value和Thread之间还存在这个强引用链路,所以导致value无法回收,就可能会出现OOM
  • JDK已经考虑到了这个问题,所以在set, remove,rehash方法中会扫描key为null的Entry,并把对应的value设置为null,这样value对象就可以被回收
  • 但是如果一个ThreadLocal不被使用,那么实际上set, remove,rehash方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就导致了value的内存泄漏
  • 共享对象: 如果在每个线程中ThreadLocal.set0进去的东西本来就是多线程共享的同一个对象,比如如static对象,那么多个线程的ThreadLocal.get0取得的还是这个共享对象本身,还是有并发访问问题
  • 如果可以不使用ThreadLocal就解决问题,那么不要强行使用
    • 例如在任务数很少的时候,在局部变量中可以新建对象就可以解决问题,那么就不需要使用到ThreadLocal
  • 优先使用框架的支持,而不是自己创造
    • 例如在Spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove0方法等,造成内存泄漏

如何避免内存泄漏(阿里规约)

  • 调用remove方法,就会删除对应的Entity对象,可以避免内存泄漏,所以使用玩ThreadLocal之后,应该调用remove方法

实际应用场景-----在Spring中的实例分析实际应用场景

  • DateTimeContextHolder类,看到里面用了ThreadLocal
  • RequestContextHolder每次HTTP请求都对应一个线程,线程之间相互隔离,这就是ThreadLocal的典型应用场景
THE END
分享
二维码
< <上一篇
下一篇>>