ThrealLocal顾名思义是提供在当前线程内存中增加并操作对象的一个工具类,也即是对当前线程内存空间的一个掌控,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同,而同一个线程在任何时候访问这个本地变量的结果都是一致的。ThreadLocal相当于提供了一种线程隔离,将变量与线程相绑定[1]。对象的绑定或者隐射很容易就让人想到这事Map类型的操作,ThreadLocal中的确对象的绑定是通过map的方式,只不过并没有直接使用到jdk中常用Map的实现。通过阅读源码,让我再一次重温了引用(Reference)[2]相关的知识。
以下分析基于jdk1.8
ThreadLocal对象的唯一性
既然对象是绑定在线程上,基于Map的设计思想,线程作为Key,绑定的对象为Value,即必须保证Key在绑定关系表上是唯一性的,ThreadLocal通过三个简单的属性轻松解决了这个问题:1
2
3
4
5
6
7
8
9
10
11
12
13// 每产生一个ThreadLocal实例,就更新下一次的 hashCode
private final int threadLocalHashCode = nextHashCode();
// 使用原子操作(CAS)并自动更新产生下一个 hashCode,保证唯一性
private static AtomicInteger nextHashCode = new AtomicInteger();
// hashCode 每次更新增量
private static final int HASH_INCREMENT = 0x61c88647;
// get and update
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
可以知道第一个ThreadLocal实例的hashCode为0(hashCode的数据类型为 int), 增量是 0x61c88647, 这已经是一个很大的数了,也就是说即之后第 N个 ThreadLocal实例的 hashCode为 (N-1)*0x61c88647
, 但第二个ThreadLocal的实例就已经超过 Integer.MAX_VALUE
了,也就是数据范围溢出了,回到Map的设计思想上来,我们都知道HashMap为什么一般使用String作为Key就是因为String的Hash算法在去重上面足够严谨(这里就不讨论了,毕竟扩展太多),也就是说,这个增量肯定是有考究的,也就考虑到在该hashCode值溢出的情况下仍能尽量保证不重复(重复是必然的,毕竟int存储范围也就 Integer.MIN_VALUE[-2147483648]~Integer.MAX_VALUE[2147483647]),通过实际计算也证明了这是一个很具有魔力的数字。既然知道了意图,那就验证一下:1
2
3
4
5
6
7
8
9
10public static void main(String[] args) throws InterruptedException {
Set<Integer> set = new HashSet<>();
int n =0;
int size = Integer.MAX_VALUE >> 6;
for(; n < size; n++) {
set.add(nextHashCode());
}
System.out.println("n: " + n + ", size: " + set.size());
}
方法有点笨,不敢size直接等于Integer.MAX_VALUE,在尝试降到 33554431(Integer.MAX_VALUE >> 6)
(此时堆的使用达到近 3.5个G,再翻个番我机器就要受不鸟了)的情况下仍能保证唯一性(数学好的可以从数学维度上给点提示,先感谢),即:1
n: 33554431, size: 33554431
ThreadLocal详情
保证了ThreadLocal对象(即Map的Key)hashCode的唯一性,作为其值(Value)的生命周期应与键(Key)一致,ThreadLocal中用来保存对象的类型是ThreadLocal.ThreadLocalMap.Entry
,这个Entry
继承java.lang.ref.WeakReference<T>
即弱引用,且将ThreadLocal对象作为弱引用类型T
,弱引用的性质就是当它没有被引用时就立即清除,而对ThreadLocalMap对象的引用是属于Thread对象中的属性,故Thread对象被destory之后该引用即失效,从而达到能被及时清除的效果。而ThradLocalMap是ThreadLocal的内部静态类,1
2
3
4
5
6
7
8
9static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocal的构造方法
构造方法体中内容为空,却注释让我们查看#withInitial(java.util.function.Supplier)
,即除了通过new ThreadLocal()
的方式,还可以通过调用 ThreadLocal的静态方法public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier)
获取ThreadLocal实例。1
2
3
4
5
6
7
8
9
10/**
* Creates a thread local variable.
* @see #withInitial(java.util.function.Supplier)
*/
public ThreadLocal() {
}
// Supplier(@FunctionalInterface)即为ThreadLocal绑定对象的产生器(since 1.8)
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
在对ThreadLocal绑定的对象的set/get方法处理上:
ThreadLocalMap的”put”
1 | public void set(T value) { |
ThreadLocalMap也是通过数组Entry[] table
的方式存放(初始化容量是16,在ThreadLocalMap对象实例化的时候指定),通过ThreadLocal的 hashCode确定对象存放的位置,与HashMap不同的是,在Hash冲突的时候,通过环的方式,从hash到的位置开始,一次向后遍历,如果发现有空的位置(可能是本来就为空的位置,或者已经过期的线程占有的位置)就将这个值存放进去(这里用的是 replace)。每有一个Entry加入,都要重新检查并调整数组的结构,即先清理一些已经无效的Entry,当ThreadLocal中没有绑定对象了,这个Entry会被认为是无效的,如果ThreadLocal引用已经失效了,但对应的Entry仍存在,则也需要将这个Entry标识为失效,失效的Entry占有的位置将会被清理出来,这也就是所谓的”启发式的垃圾清理”;当数组中的元素达到数组容量的2/3(即容量的阈值,参考Collection结构的阈值)时,对数组进行rehash,在rehash过程中又会进行一次”垃圾清理”,清理过后如果size还大于阈值的3/4,则会对存储的数组扩容。
ThreadLocalMap的”get”
1 | public T get() { |
get
方法,先通过Thread.currentThread()
获取到当前线程,通过getMap(t)
取得当前线程具有的 ThreadLocalMap对象,然后根据hash到table中获取这个Entry,如果发现没找到,即可能在容量有变更的情况下(resize过),采取getEntryAfterMiss
的方式,从应该出现的位置开始依次遍历查找,这里需要注意的是,每次遍历到k
为null的情况下,都会进行一次expungeStaleEntry(i)
操作。如果getMap(t)
获取到的值为null的情况,即还没调用set
的情况下先get
了,则会通过setInitialValue()
方法进行初始化:1
2
3
4
5
6
7
8
9
10private T setInitialValue() {
T value = initialValue(); // initialValue() 方法是提供给 ThreadLocal子类实现的初始化方法
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
即主要是创建 ThreadLocalMap对象,并把这个对象赋给当前线程的threadLocals
变量。
ThreadLocalMap的”remove”
ThreadLocal的remove方法会触发ThreadLocalMap的remove(ThreadLocal)
方法,这样就会将该Entry<ThreadLocal, Object>从ThreadLocalMap的table中清除。关于remove的描述:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 /**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* {@code initialValue} method in the current thread.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
```
# InheritableThreadLocal
在ThreadLocal的`getMap(t)`方法中可以知道`threadLocals`是Thread类的属性,与`threadLocals`属性并列的还有`inheritableThreadLocals`,InheritableThreadLocal是ThreadLocal的子类,通过名称也可以推断出来,与ThreadLocal不同的是,**InheritableThreadLocal允许一个线程以及该线程创建的所有子线程都可以访问它保存的值。**对应的,TheadLocal中也有创建InheritableThreadLocal的静态方法,且创建的对象会包含其父线程中具有的 ThreadLocalMap数据。
```java
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
总结
- ThreadLocal是提供放置一个同一个线程在任何时候访问结果都是一致的本地变量的工具。
- 线程结束,ThreadLocalMap新的元素set、set(null),remove()被调用,等情况都会导致ThreadLocalMap中的Entry对象被标识为staled(过期),从而会在新元素插入时被清理。
ThreadLocal使用到的场景很多,主要是可以用它来取代方法调用之间使用的传参传递线程中需要用到的参数,譬如单机应用的requestNo的传递,分布式锁的使用场景[3](即在释放锁操作中通过验证该锁是否为当前线程获取的锁来判断本次锁的释放是否拥有权限,如果不是当前线程获取的锁,那当前线程就没有权限去释放这个锁)。
参考:
[1] 并发编程 | ThreadLocal源码深入分析
[2] 引用类型
[3] Redis实现分布式锁