ThreadLocal使用及简单原理分析

在阅读Spring源码时, 有注意到一个类ThreadLocal出现的次数很多, 其实ThreadLocal的应用是很广泛的, 不仅仅在Spring里, 在Mybatis中也很普遍, 在一些项目的业务代码也可能会看到他的身影.

其实他的作用, 就是一个线程局部变量, 但是因为大多数的业务编程情况不常用到, 所以可能我们比较陌生, 现在分析一下, 以便在代码里看到时, 不会阻碍我们的阅读.

使用示例

//示例一
//创建, 并赋初始值
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "baseStr");

//线程1
new Thread(() -> {
    //打印默认值
    System.out.println(threadLocal.get());
    //赋值
    threadLocal.set(Thread.currentThread().getName());
    //打印值
    System.out.println(threadLocal.get());
}).start();

//线程2
new Thread(() -> {
    //打印默认值
    System.out.println(threadLocal.get());
    //赋值
    threadLocal.set(Thread.currentThread().getName());
    //打印值
    System.out.println(threadLocal.get());
}).start();
复制代码

输出值:

image.png

上述代码主要展示了创建、初始化、获取值、赋值的几个操作, 使用是很简单的.

下面主要分析比较复杂的能做什么, 怎么实现的问题.

分析特性

ThreadLocal其实比较简单, 方法也不多, 比较常用的也就是get set remove initialValue, 在分析这些方法时, 我们先要理解, ThreadLocal是用来做什么的.

源码对其的定义如下:

此类提供线程局部变量。 
这些变量不同于它们的普通对应物,
因为每个访问一个(通过其get或set方法)的线程都有自己的、独立初始化的变量副本。 
ThreadLocal实例通常是希望绑定与线程相关的类中的私有静态字段(例如,用户 ID 或事务 ID)。
复制代码

也就是说, 其主要是为了在一个线程中共享属性, 而具体是怎么实现的呢? 看下面的示例代码, 慢慢分析:

//示例二
ThreadLocal<HashMap<String, Object>> threadLocal = ThreadLocal.withInitial(() -> new HashMap<>());

new Thread(() -> {
    HashMap<String, Object> map = threadLocal.get();
    map = map == null ? new HashMap<>() : map;
    map.put("current1", Thread.currentThread().getName());
    threadLocal.set(map);
    System.out.println("t1:" + JSONObject.toJSONString(threadLocal.get()));
}).start();

new Thread(() -> {
    HashMap<String, Object> map = threadLocal.get();
    map = map == null ? new HashMap<>() : map;
    map.put("current2", Thread.currentThread().getName());
    threadLocal.set(map);
    System.out.println("t2:" + JSONObject.toJSONString(threadLocal.get()));
}).start();

System.out.println("main:" + JSONObject.toJSONString(threadLocal.get()));
复制代码

输出值:

image.png

可以看出, 两个线程对同一个threadLocal对象进行操作, 线程1和线程2同样的代码, 打印出的却是各自的值, 没有相互影响, 尤其是示例一线程2的第一句(打印默认值), 在线程1已经对threadLocal做过操作以后, 打印的还是初始化的默认值, 可见在对个线程中是不会相互污染的.

这也可以体现出其特性:线程内变量共享

实现的主要思想是, 我们定义的一个ThreadLocal在多个线程间, 会在每个线程里都创建一个副本, 每个副本是归属于线程的, 这样就做到了多个线程之间的互不干涉, 具体怎么实现一个对象在多个线程里创建副本的呢?

image.png 看上图, 由ThreaLocal的结构可以看出, ThreadLocal本身没有定义变量存储值, 那我们set的值在哪里存放呢? 先抛出概念, 后续我们看具体代码:

ThreadLocal只是做了值的映射维护, 真正的值是存储在Thread类的threadLocals字段里的

基于上面的概念, 我们不难理解, 既然值都存在每个线程的Thread类里, 那做到线程间的隔离就很正常了.

下面通过具体的方法来印证这个概念.

set方法

先看源码:

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取存储值的对象
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //不为空则set值
        map.set(this, value);
    else
        //未初始化则初始化
        createMap(t, value);
}
复制代码

这里涉及以下几个点:

  • getMap 获取值, 是从哪里获取的

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    复制代码

    可以直观的看出, 我们获取的ThreadLocalMap是从Thread中取值的.

  • createMap 为什么在获取值的时候才初始化? 初始化做了什么

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    复制代码

    很简单, 创建了一个ThreadLocalMap对象并赋值给threadLocals, 这里要主要入参: ThreadLocalMap的构造函数的第一个入参是ThreadLocal的当前对象引用

  • ThreadLocalMap 存储值的结构

    ThreadLocalMap是一种定制的哈希映射,使用 WeakReferences(Java弱引用) 作为键(Entity扩展了WeakReference)

    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        //创建条目* table是一个数组
        table = new Entry[INITIAL_CAPACITY];
        //计算下标, 并赋值
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        //记录条数
        size = 1;
        //调整容量阀值
        setThreshold(INITIAL_CAPACITY);
    }
    复制代码

    可以看出, table是在ThreadLocalMap有第一个值要存放时, 才会被创建, 这说明其是惰性创建的.

    而说ThreadLocalMap使用 WeakReferences 作为键, 是因为Entry的特性决定

        static class Entry extends WeakReference<ThreadLocal<?>> {
        
              Object value;
    
              Entry(ThreadLocal<?> k, Object v) {
                  super(k);
                  value = v;
              }
          }
    复制代码

    可看出, Entry扩展了WeakReference(Java弱引用), 而构造Entry的构造函数, key是ThreadLocal对象

    这里涉及到了WeakReference相关的知识, 在这里不做过多说明

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;
        }
    }
    //如果ThreadLocalMap为空, 或对应Entity为空, 则进行初始化
    return setInitialValue();
}
复制代码

有get方法的研究可知获取线程和getMap(t)所做的工作, 这里不再重复, 下面几个地方我们要分析一下:

  • 获取Entry及获取Entry关联的值

    我们通过上面的分析, 可以知道:ThreadLocalMap的存储, 和Entry的存储, 获取就是通过存储的相同规则去反向获取值, 例如map.getEntry(this)使用当前ThreadLocal自身的引用.

  • setInitialValue方法做了什么

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}
复制代码

setInitialValue方法用做获取值时仍旧未初始化的情况下初始化, 这个方法是set方法的变体, 所以基本和set方法一致, 但仍有不同, 如下:

    • value的值由指定

      这里的initialValue是可以自己覆盖的, 当我们手动指定了初始化的值时(我们只需要覆盖initialValue方法), 在第一次获取时, 如果未设置值, 就会使用我们覆盖的initialValue方法的返回值来初始化. initialValue方法默认是返回null的.

         protected T initialValue() {
             return null;
         }
      复制代码
    • 有返回值, 返回的是initialValue返回的值

由此, ThreadLocal的主要特性已经展现了, 我们再分析几个常用的方法, 更全面的认识ThreadLocal类.

remove方法

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}
复制代码

remove方法用来移除当前set的值, 在移除后再次通过get获取, 会重新初始化initialValue方法

initialValue方法

在get()方法中已经介绍, 这里展示两种覆盖的常用方法

  • 匿名内部类
ThreadLocal<String> threadLocal = new ThreadLocal<String>(){
    @Override
    protected String initialValue() {
        return "base";
    }
};
复制代码
  • withInitial
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "baseStr");
复制代码

withInitial方法

上述示例里用到了ThreadLocal.withInitial覆盖initialValue, 怎么实现的呢? 分析一下:

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}
复制代码

我们可以看到通过Supplier, 返回了一个SuppliedThreadLocal, 分析下SuppliedThreadLocal

//扩展了ThreadLocal
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

    private final Supplier<? extends T> supplier;

    SuppliedThreadLocal(Supplier<? extends T> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }

    //覆盖了ThreadLocal的initialValue方法
    @Override
    protected T initialValue() {
        return supplier.get();
    }
}
复制代码

可见SuppliedThreadLocal其实也是扩展了ThreadLocal且覆盖了initialValue方法, 这些都可以和我们上述的分析对应上, 那么supplier.get()是怎么返回ThreadLocal.withInitial(() -> "baseStr")中的baseStr的呢?

@FunctionalInterface
public interface Supplier<T> {
    T get();
}
复制代码

可见Supplier是个函数式接口, 那么

ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "baseStr");
复制代码

等价于

ThreadLocal<String> threadLocal = ThreadLocal.withInitial(new Supplier<String>() {
    @Override
    public String get() {
        return "baseStr";
    }
});
复制代码

现在看起来, 就顺理成章多了

这里提出这个, 和ThreadLocal无关, 只是避免lambda表达式对阅读分析起到干扰, 故而提及


后续, 将会列举一些经典的使用案例, 对于原理分析就记录到这.

Guess you like

Origin juejin.im/post/7031562482711805982