在阅读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();
复制代码
输出值:
上述代码主要展示了创建、初始化、获取值、赋值的几个操作, 使用是很简单的.
下面主要分析比较复杂的能做什么, 怎么实现的问题.
分析特性
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()));
复制代码
输出值:
可以看出, 两个线程对同一个threadLocal对象进行操作, 线程1和线程2同样的代码, 打印出的却是各自的值, 没有相互影响, 尤其是示例一线程2的第一句(打印默认值), 在线程1已经对threadLocal做过操作以后, 打印的还是初始化的默认值, 可见在对个线程中是不会相互污染的.
这也可以体现出其特性:线程内变量共享
实现的主要思想是, 我们定义的一个ThreadLocal在多个线程间, 会在每个线程里都创建一个副本, 每个副本是归属于线程的, 这样就做到了多个线程之间的互不干涉, 具体怎么实现一个对象在多个线程里创建副本的呢?
看上图, 由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表达式对阅读分析起到干扰, 故而提及
后续, 将会列举一些经典的使用案例, 对于原理分析就记录到这.