ThreadLocal使用及简单原理分析

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

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

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码//示例一
//创建, 并赋初始值
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是用来做什么的.

源码对其的定义如下:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码//示例二
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方法

先看源码:

1
2
3
4
5
6
7
8
9
10
11
12
Java复制代码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 获取值, 是从哪里获取的
1
2
3
java复制代码ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

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

  • createMap 为什么在获取值的时候才初始化? 初始化做了什么
1
2
3
java复制代码void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

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

  • ThreadLocalMap 存储值的结构

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

1
2
3
4
5
6
7
8
9
10
11
java复制代码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的特性决定

1
2
3
4
5
6
7
8
9
scala复制代码    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方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码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方法做了什么
1
2
3
4
5
6
7
8
9
10
Java复制代码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的.



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

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

remove方法

1
2
3
4
5
Java复制代码public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

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

initialValue方法

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

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

withInitial方法

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Java复制代码//扩展了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的呢?

1
2
3
4
csharp复制代码@FunctionalInterface
public interface Supplier<T> {
T get();
}

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

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

等价于

1
2
3
4
5
6
typescript复制代码ThreadLocal<String> threadLocal = ThreadLocal.withInitial(new Supplier<String>() {
@Override
public String get() {
return "baseStr";
}
});

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

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


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

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%