在阅读Spring源码时, 有注意到一个类ThreadLocal
出现的次数很多, 其实ThreadLocal
的应用是很广泛的, 不仅仅在Spring里, 在Mybatis中也很普遍, 在一些项目的业务代码也可能会看到他的身影.
其实他的作用, 就是一个线程局部变量, 但是因为大多数的业务编程情况不常用到, 所以可能我们比较陌生, 现在分析一下, 以便在代码里看到时, 不会阻碍我们的阅读.
使用示例
1 | java复制代码//示例一 |
输出值:
上述代码主要展示了创建、初始化、获取值、赋值的几个操作, 使用是很简单的.
下面主要分析比较复杂的能做什么, 怎么实现的问题.
分析特性
ThreadLocal
其实比较简单, 方法也不多, 比较常用的也就是get
set
remove
initialValue
, 在分析这些方法时, 我们先要理解, ThreadLocal
是用来做什么的.
源码对其的定义如下:
1 | arduino复制代码此类提供线程局部变量。 |
也就是说, 其主要是为了在一个线程中共享属性, 而具体是怎么实现的呢? 看下面的示例代码, 慢慢分析:
1 | java复制代码//示例二 |
输出值:
可以看出, 两个线程对同一个threadLocal对象进行操作, 线程1和线程2同样的代码, 打印出的却是各自的值, 没有相互影响, 尤其是示例一线程2的第一句(打印默认值), 在线程1已经对threadLocal做过操作以后, 打印的还是初始化的默认值, 可见在对个线程中是不会相互污染的.
这也可以体现出其特性:线程内变量共享
实现的主要思想是, 我们定义的一个ThreadLocal在多个线程间, 会在每个线程里都创建一个副本, 每个副本是归属于线程的, 这样就做到了多个线程之间的互不干涉, 具体怎么实现一个对象在多个线程里创建副本的呢?
看上图, 由ThreaLocal的结构可以看出, ThreadLocal本身没有定义变量存储值, 那我们set的值在哪里存放呢?
先抛出概念, 后续我们看具体代码:
ThreadLocal只是做了值的映射维护, 真正的值是存储在Thread类的threadLocals字段里的
基于上面的概念, 我们不难理解, 既然值都存在每个线程的Thread类里, 那做到线程间的隔离就很正常了.
下面通过具体的方法来印证这个概念.
set方法
先看源码:
1 | Java复制代码public void set(T value) { |
这里涉及以下几个点:
- getMap 获取值, 是从哪里获取的
1 | java复制代码ThreadLocalMap getMap(Thread t) { |
可以直观的看出, 我们获取的ThreadLocalMap是从Thread中取值的.
- createMap 为什么在获取值的时候才初始化? 初始化做了什么
1 | java复制代码void createMap(Thread t, T firstValue) { |
很简单, 创建了一个ThreadLocalMap对象并赋值给threadLocals, 这里要主要入参: ThreadLocalMap的构造函数的第一个入参是ThreadLocal的当前对象引用
- ThreadLocalMap 存储值的结构
ThreadLocalMap是一种定制的哈希映射,使用 WeakReferences(Java弱引用) 作为键(Entity扩展了WeakReference)
1 | java复制代码ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { |
可以看出, table是在ThreadLocalMap有第一个值要存放时, 才会被创建, 这说明其是惰性创建
的.
而说ThreadLocalMap使用 WeakReferences 作为键, 是因为Entry的特性决定
1 | scala复制代码 static class Entry extends WeakReference<ThreadLocal<?>> { |
可看出, Entry扩展了WeakReference(Java弱引用), 而构造Entry的构造函数, key是ThreadLocal对象
这里涉及到了WeakReference相关的知识, 在这里不做过多说明
get方法
1 | java复制代码public T get() { |
有get方法的研究可知获取线程和getMap(t)所做的工作, 这里不再重复, 下面几个地方我们要分析一下:
- 获取Entry及获取Entry关联的值
我们通过上面的分析, 可以知道:ThreadLocalMap的存储, 和Entry的存储, 获取就是通过存储的相同规则去反向获取值, 例如map.getEntry(this)使用当前ThreadLocal自身的引用.
- setInitialValue方法做了什么
1 | Java复制代码private T setInitialValue() { |
setInitialValue方法用做获取值时仍旧未初始化的情况下初始化, 这个方法是set方法的变体, 所以基本和set方法一致, 但仍有不同, 如下:
- value的值由指定
这里的initialValue是可以自己覆盖的, 当我们手动指定了初始化的值时(我们只需要覆盖initialValue方法), 在第一次获取时, 如果未设置值, 就会使用我们覆盖的initialValue方法的返回值来初始化.
initialValue方法默认是返回null的.
1
2
3
Java复制代码 protected T initialValue() {
return null;
}
- 有返回值, 返回的是initialValue返回的值
由此, ThreadLocal的主要特性已经展现了, 我们再分析几个常用的方法, 更全面的认识ThreadLocal类.
remove方法
1 | Java复制代码public void remove() { |
remove方法用来移除当前set的值, 在移除后再次通过get获取, 会重新初始化initialValue方法
initialValue方法
在get()方法中已经介绍, 这里展示两种覆盖的常用方法
- 匿名内部类
1 | Java复制代码ThreadLocal<String> threadLocal = new ThreadLocal<String>(){ |
- withInitial
1 | Java复制代码ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "baseStr"); |
withInitial方法
上述示例里用到了ThreadLocal.withInitial覆盖initialValue, 怎么实现的呢? 分析一下:
1 | Java复制代码public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) { |
我们可以看到通过Supplier, 返回了一个SuppliedThreadLocal, 分析下SuppliedThreadLocal
1 | Java复制代码//扩展了ThreadLocal |
可见SuppliedThreadLocal其实也是扩展了ThreadLocal且覆盖了initialValue方法, 这些都可以和我们上述的分析对应上, 那么supplier.get()
是怎么返回ThreadLocal.withInitial(() -> “baseStr”)中的baseStr的呢?
1 | csharp复制代码@FunctionalInterface |
可见Supplier是个函数式接口, 那么
1 | ini复制代码ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "baseStr"); |
等价于
1 | typescript复制代码ThreadLocal<String> threadLocal = ThreadLocal.withInitial(new Supplier<String>() { |
现在看起来, 就顺理成章多了
这里提出这个, 和ThreadLocal无关, 只是避免lambda表达式对阅读分析起到干扰, 故而提及
后续, 将会列举一些经典的使用案例, 对于原理分析就记录到这.
本文转载自: 掘金