简述ThreadLocal原理

50 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第13天,点击查看活动详情

前言

我们经常听说ThreadLocal可以解决线程安全的问题,但是却不知道它是如何做到的,为了了解一下ThreadLocal的设计思路,我们今天这篇文章通过源码来看一下它是如何实现线程安全的;

使用示例

ThreadLocal的使用特别简单,通过下面这一段代码,开发人员就可以在项目当中使用set()get()remove()方法了:

private static ThreadLocal<StringTHREAD_LOCAL = new ThreadLocal<>();
​
public static void set(String data) {
THREAD_LOCAL.set(data);
}
​
public static String get() {
return THREAD_LOCAL.get();
}
​
public static void remove() {
THREAD_LOCAL.remove();
}

它经常被用来保存上下文,比如spring中使用它来保存事务的上下文,seata中用ThreadLocal来保存全局分布式事务的XID等等;

源码解析

为了了解ThreadLocal是如何做到线程安全的,我们需要去看一下它的源码,先来看一下set()方法:

   public void set(T value) {
       // 获取当前线程
       Thread t = Thread.currentThread();
       // 从线程中取出ThreadLocalMap
       ThreadLocalMap map = getMap(t);
       if (map != null) {
           // 把ThreadLocal作为key,把value放进ThreadLocalMap中
           map.set(this, value);
      } else {
           // 创建一个ThreadLocalMap放进当前线程中
           createMap(t, value);
      }
  }

通过上面的代码,我们就明白了,原理ThreadLocal是通过给每一个线程里面的ThreadLocalMap都放一份数据来保证线程安全的;

image-20221210182507866.png

通过上图我们可以很明显地知道,每个线程中都有属于自己的value值,虽然它们都共用同一个ThreadLocal实例作为key

我们再来看一下get()方法:

   public T get() {
       // 获取当前线程
       Thread t = Thread.currentThread();
       // 取出线程中的ThreadLocalMap
       ThreadLocalMap map = getMap(t);
       if (map != null) {
           // 通过ThreadLocal实例作为key取出里面的元素
           ThreadLocalMap.Entry e = map.getEntry(this);
           if (e != null) {
               // 取出value并返回
               @SuppressWarnings("unchecked")
               T result = (T)e.value;
               return result;
          }
      }
       // 如果map为null,那么就执行初始化操作
       return setInitialValue();
  }

这个和set()方法如出一辙,因为是通过TheadLocal作为key存进去的,所以取数据的时候也需要通过ThreadLocal作为key来取数据;ThreadLocal还提供给我们initialValue()方法让我们在没有数据的时候可以初始化数据,所以如果mapnull的情况下,还可以实现自定义初始化数据;

下面我们再来看看remove()方法:

    public void remove() {
        // 获取当前线程中的ThreadLocalMap
        ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null) {
            // map不为null,异常对应的数据
            m.remove(this);
        }
    }

这里特别需要注意的就是,在线程的业务逻辑处理完毕后,一定要记得执行remove()方法把数据删掉;如果该线程是线程池中的线程,存在复用的情况下,会导致上下文污染的情况,更严重的可能会造成程序的OOM