转载请注明原文地址:https://blog.csdn.net/yu749942362/article/details/107014198
ThreadLocal的使用和实现原理
1,ThreadLocal的使用
ThreadLocal
提供线程局部变量。这些变量与它们的普通对应变量的不同之处在于,每个通过其get
或set
方法访问变量的线程都有自己独立初始化的变量副本。实例通常是类中希望将状态与线程(例如,用户ID或事务ID)关联的私有静态字段。
每个线程都有一个对其线程局部变量副本的隐式引用,只要线程是活着的并且ThreadLocal实例是可访问的;在一个线程离开后,它的线程本地实例的所有副本都将受到垃圾收集的影响(除非存在对这些副本的其他引用)。
ThreadLocal
的使用很简单,SDK暴露出可供调用的方法只有三个:
除此之外还应关注一个重要的方法:initialValue
。
这四个方法的用处分别是:
-
initialValue
:返回当前线程的线程局部变量的“初始值”。该方法将在线程第一次使用get
方法访问变量时被调用,除非线程之前调用了set
方法。通常,这个方法在每个线程中最多调用一次,但是在调用了remove
之后再调用get
,会再次调用该方法。 -
set
:为此线程的线程局部变量设置指定值。 -
get
:返回此线程局部变量的当前线程副本中的值。如果该变量没有值,则首先将其初始化为调用initialValue
方法的返回值。 -
remove
:删除此线程局部变量的当前线程值。
举个简单的例子,下面的类为每个线程生成一个本地的唯一名称标识。
public class ThreadLocalTest extends Thread {
private static final int COUNT = 20;
private static final AtomicInteger next = new AtomicInteger(0);
private static final CountDownLatch countDownLatch = new CountDownLatch(COUNT);
private ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return next.incrementAndGet();
}
};
@Override
public void run() {
setName("Thread:" + threadLocal.get());
countDownLatch.countDown();
}
public static void main(String[] args) {
List<ThreadLocalTest> list = new ArrayList<>(COUNT);
for (int i = 0; i < COUNT; i++) {
ThreadLocalTest threadLocalTest = new ThreadLocalTest();
threadLocalTest.start();
list.add(threadLocalTest);
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (ThreadLocalTest threadLocalTest : list) {
System.out.println(threadLocalTest.getName());
}
}
}
观察一下运行结果
线程的Name在每次调用threadLocal.get()
时被分配,并且在随后的调用中尽管next.incrementAndGet()
已经发生改变,但threadLocal.get()
始终保持不变。
2,ThreadLocal的实现
initialValue
、set
、get
和remove
。
2.1, 初始值 — initialValue()
源码给出的注释很详细:initialValue
返回当前线程的线程局部变量的“初始值”。该方法通常只会在线程第一次使用get
方法访问变量时被调用,除非线程之前调用了set
方法,在这种情况下,不会为该线程调用initialValue
方法。通常,每个线程最多调用该方法一次,但是在后续调用remove
和get
时,可能会再次调用该方法。
protected T initialValue() {
return null;
}
此方法只在方法private T setInitialValue()
中调用,而setInitialValue也仅在get中被调用过。
initialValue
的默认实现只返回了一个null
。如果我们希望线程局部变量有一个非空的初始值,就需要重写initialValue
方法(ThreadLocal
子类化或使用匿名内部类重写此方法)。
2.2, 值的设置 — set(T value)
set(T value)
的作用是将此线程局部变量的当前线程副本设置为指定值。
/**
* 大多数子类不需要重写这个方法,只需要依赖{@link #initialValue}方法来设置线程局部变量的值。
* @param value 存储在当前线程此线程的副本中的值—local。
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
在getMap中只有一行代码,就是返回当前线程中的threadLocals
(ThreadLocalMap
)对象。
这里引入了一个采用 线性探测法 来解决哈希冲突的哈希表实现——ThreadLocalMap。
线性探测法 的地址增量di = 1, 2, … , m-1,其中,i为探测次数。该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。假设当前table长度为16,也就是说假如计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。
ThreadLocalMap
是一个定制的哈希映射,只适用于维护线程本地值。没有操作被导出到ThreadLocal
类之外。这个类是包私有的,允许在类线程中声明字段。为了帮助处理非常大的和长期存在的使用,这个哈希表项对keys使用WeakReferences
。但是,由于不使用引用队列,因此只有当表空间开始耗尽时,才保证删除陈旧的条目。
/**
* 散列映射中的条目继承至WeakReference,使用其主ref字段作为键(始终是ThreadLocal对象)。
* 注意,空键(即entry.get() == null)意味着不再引用该键,因此可以从表中删除该条目。
* 这样的条目在下面的代码中称为“陈旧条目”。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
回到 set(T value)
方法。如果当前线程的threadLocals
不为空,则调用其set(this, value)
为当前线程的threadLocals
设值为value
;如果为空则调用createMap(t, value)
为当前线程创建一个类型为ThreadLocalMap
的threadLocals
。
常见解决哈希冲突时使用 拉链法 或 线性探测法的较多,而HashMap中使用的则是拉链法。有兴趣的朋友可以看看《阅读HashMap源码时你可能会有这些疑问》。后文会简单介绍下ThreadLocalMap
中的线性探测法的实现。
2.3, 值的获取 — get()
/**
* 返回此线程局部变量的当前线程副本中的值。如果该变量没有当前线程的值,
* 则首先将其初始化为initialValue方法返回的值。
*/
public T get() {
Thread t = Thread.currentThread();
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;
}
}
return setInitialValue();
}
首先获取当前线程的threadLocals
以map
接收。如果map
值不为空,则取其value并返回;如果map==null
则调用setInitialValue()
。setInitialValue
相比较于set(T value)
只多出了一行,即首行调用了initialValue()
。这刚好印证了“每个线程最多调用该方法一次”。
2.4, 值的移除 — remove()
获取当前线程的ThreadLocalMap
并移除key为this
的元素。
/**
* 删除此线程局部变量的当前线程值。
* 如果这个线程局部变量随后被当前线程get所读取,它的值将通过调用它的initialValue方法重新初始化,
* 除非它的值在过渡期间被当前线程调用set。这可能导致在当前线程中多次调用initialValue方法。
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
3,ThreadLocalMap 之线性探测法
以ThreadLocalMap
中的set(ThreadLocal<?> key, Object value)
方法为例,在阅读其源码之前先看ThreadLocal
中的两个全局变量:
/**
* 线程局部变量依赖于附加到每个线程的线性探测哈希映射(threadlocal和inheritablethreadlocal)
* ThreadLocal对象充当键,通过threadLocalHashCode搜索。
* 这是一个定制的哈希码(仅在ThreadLocalMaps中有用),它在一般情况下消除了冲突,
* 即相同线程使用连续构造的线程局部变量,而在不太常见的情况下保持良好的行为。
*/
private final int threadLocalHashCode = nextHashCode();
/**
* 下一个要给出的哈希码。从0开始,按照增量HASH_INCREMENT自增。
*/
private static AtomicInteger nextHashCode = new AtomicInteger();
threadLocalHashCode
是通过 nextHashCode
得到的。顾名思义,返回下一个哈希码。
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
HASH_INCREMENT = 0x61c88647
连续生成的哈希码之间的差异——将隐式顺序 thread-local的id转换为接近最优扩散的倍增哈希值,用于两级幂的表。至于为什么是这个值目前还不清楚。
/**
* 设置与key关联的值
*/
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 计算出key在table数组中的下标位置i,为什么这么写?可以看我的另一篇博文,下方有链接
int i = key.threadLocalHashCode & (len-1);
// 假如i位置在table中已经有值了,则调用nextIndex找寻i+1的位置,依次类推,直到找到一个为空的位置。
// 然后将值放入其中,如果i+1超出了table的容量会折回下标为0的位置开始继续寻找。
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
// 从下方代码可以看到,每次set之后都会检查map中元素数量是否超出阈值,
// 如果超出阈值会调用rehash进行扩容,并通过setThreshold设置新的阈值,
// 所以不用担心会因为一致找不到可用的位置而陷入死循环。
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}