ThreadLocal

发布时间 2023-04-08 14:18:18作者: 李勇888

什么是ThreadLocal

  • ThreadLocal提供了线程局部变量. 这些变量和正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候 都有自己独立的 变量副本.
  • ThreadLocal实例通常是类的私有静态字段,使用它的目的是希望将状态(用户ID、事务ID) 与线程关联起来
  • 通俗易懂: 实现每一个线程都有自己的专属本地变量副本

ThreadLocal能解决什么问题

  • 我们知道解决线程安全问题,要么用synchronzied、ReentrantLock、或者用原子类, 这类解决方法说白了都是通过加锁的方式
  • 而 ThreadLocal则是 每个线程都存有自己的本地变量副本,各玩各的 互不打扰

ThreadLocal需要注意的点

  • 使用完ThreadLocal一定要手动的remove

    • 如果是使用线程池处理业务,会存在线程复用的情况,上一段业务处理的结果没有清空 会影响到下一段业务,造成数据错乱,严重的会造成内存溢出
    • 代码
      • 下面的submit执行逻辑 如果没有手动remove,就会导致累加操作无限下去,
      class Number {
          ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
      
          public void add(){
              threadLocal.set(threadLocal.get() + 1);
          }
      }
      
      public class ThreadLocal003 {
      
          public static void main(String[] args) {
              ExecutorService executorService = Executors.newFixedThreadPool(3);
      
              Number number = new Number();
      
              for (int i = 0;i < 10;i++){
                  executorService.submit(() -> {
                      try {
                          Integer beforeInt = number.threadLocal.get();
                          number.add();
                          Integer afterInt = number.threadLocal.get();
                          System.out.println(Thread.currentThread().getName() + "\t\t beforeInt: " + beforeInt + "---afterInt: " + afterInt);
                      } finally {
                          number.threadLocal.remove();
                      }
                  });
              }
      
          }
      }
      
  • 为什么ThreadLocal会有内存泄露的风险

    • 什么是内存泄漏

      • 不再会被使用的对象或者变量占用的内存不能被回收
    • 弱引用修饰ThreadLocal 保证了 Entry对象中的key 指向ThreadLocal对象能被及时回收,但是 v指向的对象是需要调用set get 方法发现key是null才会回收整个Entry, 因此弱引用不能百分比解决内存泄漏问题.

      • 我们不使用某个ThreadLocal对象后,一定要调用remove方法删除它
      • 尤其是线程池中,不仅仅是内存泄漏的问题,因为线程池中的线程是复用的,意味着线程的ThreadLocalMap对象也是重复使用的,如果不手动调用remove方法,那么后面的线程就有可能获取到上一个线程遗留下来的value值,造成bug
  • 为什么ThreadLocalMap 里的entry对象 需要继承WeakReference(弱引用)

    • 栈 强引用指向ThreadLocal 对象, ThreadLocalMap 引用指向ThreadLocal对象
    • 如果是entry对象是强引用 指向ThreadLocal, 一旦栈的强引用被回收后, entry对象key指向ThreadLocal不能被gc回收,造成内存泄漏
    • 如果是弱引用,就会减少内存泄漏的问题

ThreadLocal源码分析

  • Thread、ThreadLocal、ThreadLocalMap之间的关系

    • ThreadLocalMap是ThreadLocal的静态内部类
    • Thread类中存在 ThreadLocalMap属性
      image.png
    • ThreadLocalMap 是一个保存ThreadLocal对象的map(ThreadLocal作为key)
  • 属性说明

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    
  • 公共方法

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    private T setInitialValue() {
        
        // 初始值是null
        T value = initialValue();
        Thread t = Thread.currentThread();
        
        // 获取map
        // map如果是null 则创建map,key是ThreadLocal value是 初始值value 长度大小默认是18
        // map不是null,则 put进去
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
    
  • get方法

    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();
    }
    

总结、

  • ThreadLocal并不解决线程间共享数据的问题
  • 适用于变量在线程间隔离且在方法间共享的场景
  • ThreadLocal在各个线程创建了独立的变量副本避免了线程安全问题
  • ThreadLocalMap的Entry对 ThreadLocal的引用为弱引用, 避免了ThreadLocal无法被回收的问题
  • get set remove方法都会通过exoungStaleEntry方法回收k = null的键, 从而防止value内存泄漏