ThreadLocal详解

大家好,最近工作中有和 ThreadLocal 打交道,翻了翻源码,就总结了下 ThreadLocal 的底层原理,如何保证线程之间共享变量的安全性。

1. ThreadLocal 是什么?为什么要使用 ThreadLocal

1.1 ThreadLocal 是什么?

ThreadLocal,即线程本地变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量时,实际是在操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了并发场景下的线程安全问题。

//创建一个ThreadLocal变量
static ThreadLocal<String> localVariable = new ThreadLocal<>();

1.2 为什么要使用 ThreadLocal 呢?

并发场景下,会存在多个线程同时修改一个共享变量的场景,这就可能会出现线性安全问题。

为了解决线性安全问题,可以用加锁的方式,比如使用synchronized 或者Lock。但是加锁的方式,可能会导致系统变慢。加锁示意图如下:

 还有另外一种方案,就是使用空间换时间的方式,即使用ThreadLocal。使用ThreadLocal类访问共享变量时,会在每个线程的本地,都保存一份共享变量的拷贝副本。多线程对共享变量修改时,实际上操作的是这个变量副本,从而保证线性安全。

2. 一个ThreadLocal的使用案例

日常开发中,ThreadLocal经常在日期转换工具类中出现,我们先来看个反例:

/**
 * 日期工具类
 */
public class DateUtil {

    private static final SimpleDateFormat simpleDateFormat =
            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static Date parse(String dateString) {
        Date date = null;
        try {
            date = simpleDateFormat.parse(dateString);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }

   // 我们在多线程环境跑DateUtil这个工具类:
  public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 10; i++) {
            executorService.execute(()->{
                System.out.println(DateUtil.parse("2022-07-24 16:34:30"));
            });
        }
        executorService.shutdown();
    }
}

运行后,发现报错了:

 如果在DateUtil工具类,加上ThreadLocal,运行则不会有这个问题:

/**
 * 日期工具类
 */
public class DateUtil {

    private static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static Date parse(String dateString) {
        Date date = null;
        try {
            date = dateFormatThreadLocal.get().parse(dateString);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 10; i++) {
            executorService.execute(()->{
                System.out.println(DateUtil.parse("2022-07-24 16:34:30"));
            });
        }
        executorService.shutdown();
    }
}

运行结果:

Sun Jul 24 16:34:30 GMT+08:00 2022
Sun Jul 24 16:34:30 GMT+08:00 2022
Sun Jul 24 16:34:30 GMT+08:00 2022
Sun Jul 24 16:34:30 GMT+08:00 2022
Sun Jul 24 16:34:30 GMT+08:00 2022
Sun Jul 24 16:34:30 GMT+08:00 2022
Sun Jul 24 16:34:30 GMT+08:00 2022
Sun Jul 24 16:34:30 GMT+08:00 2022
Sun Jul 24 16:34:30 GMT+08:00 2022
Sun Jul 24 16:34:30 GMT+08:00 2022

刚刚反例中,为什么会报错呢?这是因为SimpleDateFormat不是线性安全的,它以共享变量出现时,并发多线程场景下即会报错。

为什么加了ThreadLocal就不会有问题呢?并发场景下,ThreadLocal是如何保证的呢?我们接下来看看ThreadLocal的核心原理。

3. ThreadLocal的原理

3.1 ThreadLocal的内存结构图

为了有个宏观的认识,我们先来看下ThreadLocal的内存结构图:

 

从内存结构图,我们可以看到:

  • Thread类中,有个 ThreadLocal.ThreadLocalMap 的成员变量。
  • ThreadLocalMap 内部维护了 Entry 数组,每个 Entry 代表一个完整的对象,key是T hreadLocal 本身,value 是 ThreadLocal 的泛型对象值。

3.2 关键源码分析

3.2 关键源码分析

对照着几段关键源码来看,更容易理解一点哈~我们回到Thread类源码,可以看到成员变量ThreadLocalMap的初始值是为null

public class Thread implements Runnable {
   //ThreadLocal.ThreadLocalMap是Thread的属性
   ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocalMap的关键源码如下:

static class ThreadLocalMap {
    
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    //Entry数组
    private Entry[] table;
    
    // ThreadLocalMap的构造器,ThreadLocal作为key
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }
}

ThreadLocal类中的关键set()方法:

public void set(T value) {
        Thread t = Thread.currentThread(); //获取当前线程t
        ThreadLocalMap map = getMap(t);  //根据当前线程获取到ThreadLocalMap
        if (map != null)  //如果获取的ThreadLocalMap对象不为空
            map.set(this, value); //K,V设置到ThreadLocalMap中
        else
            createMap(t, value); //创建一个新的ThreadLocalMap
    }
    
     ThreadLocalMap getMap(Thread t) {
       return t.threadLocals; //返回Thread对象的ThreadLocalMap属性
    }

    void createMap(Thread t, T firstValue) { //调用ThreadLocalMap的构造函数
        t.threadLocals = new ThreadLocalMap(this, firstValue);  // this表示当前类ThreadLocal
    }

ThreadLocal类中的关键get()方法:

 public T get() {
        Thread t = Thread.currentThread();//获取当前线程t
        ThreadLocalMap map = getMap(t);//根据当前线程获取到ThreadLocalMap
        if (map != null) { //如果获取的ThreadLocalMap对象不为空
            //由this(即ThreadLoca对象)得到对应的Value,即ThreadLocal的泛型值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value; 
                return result;
            }
        }
        return setInitialValue(); //初始化threadLocals成员变量的值
    }
    
     private T setInitialValue() {
        T value = initialValue(); //初始化value的值
        Thread t = Thread.currentThread(); 
        ThreadLocalMap map = getMap(t); //以当前线程为key,获取threadLocals成员变量,它是一个ThreadLocalMap
        if (map != null)
            map.set(this, value);  //K,V设置到ThreadLocalMap中
        else
            createMap(t, value); //实例化threadLocals成员变量
        return value;
    }

ThreadLocal的实现原理:最好是能结合以上结构图一起说明:

  • Thread线程类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。
  • ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。(备注:一个线程Thread内,只会有一个 ThreadLocal.ThreadLocalMap 对象,但是可以创建多个 ThreadLocal 对象用来存储数据,这些 ThreadLocal 不同的对象可以通过每个 ThreadLocal 对象的 HashCode 值作为key,存储在 ThreadLocal.ThreadLocalMap 中)
  • 并发多线程场景下,每个线程Thread,在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而可以实现了线程隔离。

了解完这几个核心方法后,有些小伙伴可能会有疑惑,ThreadLocalMap为什么要用ThreadLocal作为key呢?直接用线程Id不一样嘛?

4. 为什么不直接用线程id作为ThreadLocalMap的key呢?

举个代码例子,如下:

public class TianLuoThreadLocalTest {

    private static final ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
    private static final ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
 
}

这种场景:一个使用类,有两个共享变量,也就是说用了两个ThreadLocal成员变量的话。如果用线程id作为ThreadLocalMap的key,怎么区分哪个ThreadLocal成员变量呢?因此还是需要使用ThreadLocal作为Key来使用。每个ThreadLocal对象,都可以由threadLocalHashCode属性唯一区分的,每一个ThreadLocal对象都可以由这个对象的名字唯一区分(下面的例子)。看下ThreadLocal代码:

public class ThreadLocal<T> {
  private final int threadLocalHashCode = nextHashCode();
  
  private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
  }
}

看下一个代码例子(同一个 Thread 对象里面,使用 ThreadLocalMap 存储多个 ThreadLocal 对象的值):

public static void main(String[] args) {
    Thread t = new Thread(() -> {
        ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
        threadLocal1.set("学习ThreadLocal1");
        System.out.println(threadLocal1.get());
        ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
        threadLocal2.set("学习ThreadLocal2");
        System.out.println(threadLocal2.get());
    });
    t.start();
}

// 输出结果:
学习ThreadLocal1
学习ThreadLocal2

再对比下这个图,可能就更清晰一点啦:

 

5. TreadLocal为什么会导致内存泄漏呢?

5.1 弱引用导致的内存泄漏呢?

我们先来看看TreadLocal的引用示意图哈:

关于ThreadLocal内存泄漏,网上比较流行的说法是这样的:

ThreadLocalMap使用ThreadLocal的弱引用作为key,当ThreadLocal变量被手动设置为null,即一个ThreadLocal没有外部强引用来引用它,当系统GC时,ThreadLocal一定会被回收。
这样的话,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如线程池的核心线程),
这些key为null的Entry的value就会一直存在一条强引用链:Thread变量 -> Thread对象 -> ThreaLocalMap -> Entry -> value -> Object 永远无法回收,造成内存泄漏。

当ThreadLocal变量被手动设置为null后的引用链图:

 实际上,ThreadLocalMap的设计中已经考虑到这种情况。所以也加上了一些防护措施:即在ThreadLocal的get,set,remove方法,都会清除线程ThreadLocalMap里所有key为null的value,

源代码中,是有体现的,如ThreadLocalMap的set方法:

private void set(ThreadLocal<?> key, Object value) {

      Entry[] tab = table;
      int len = tab.length;
      int i = key.threadLocalHashCode & (len-1);

      for (Entry e = tab[i];
            e != null;
            e = tab[i = nextIndex(i, len)]) {
          ThreadLocal<?> k = e.get();

          if (k == key) {
              e.value = value;
              return;
          }

           //如果k等于null,则说明该索引位之前放的key(threadLocal对象)被回收了,这通常是因为外部将threadLocal变量置为null,
           //又因为entry对threadLocal持有的是弱引用,一轮GC过后,对象被回收。
            //这种情况下,既然用户代码都已经将threadLocal置为null,那么也就没打算再通过该对象作为key去取到之前放入threadLocalMap的value, 因此ThreadLocalMap中会直接替换调这种不新鲜的entry。
          if (k == null) {
              replaceStaleEntry(key, value, i);
              return;
          }
        }

        tab[i] = new Entry(key, value);
        int sz = ++size;
        //触发一次Log2(N)复杂度的扫描,目的是清除过期Entry  
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
          rehash();
    }

参考CSDN博客:https://blog.csdn.net/pxg943055021/article/details/124690435?share_token=b854cf8e-5ab1-4203-a9ee-703912dd62b2

 

posted @ 2025-07-13 11:15  菜鸟的奋斗之路  阅读(154)  评论(0)    收藏  举报