在平常开发的时候,经常使用到线程本地变量,这种类型的变量会在每个线程中都有一份,互相不会产生影响,这样来解决多线程并发问题。
那么是如何实现的呢?
一. ThreadLocal<T>
1.1 例子
private static final ThreadLocal<AtomicInteger> threadLocal = new ThreadLocal<AtomicInteger>(){
@Override
protected AtomicInteger initialValue() {
AtomicInteger result = new AtomicInteger(0);
System.out.println("创建 AtomicInteger("+result.hashCode()+") thread:"+Thread.currentThread().getName());
return result;
}
};
public static void main(String[] args) {
for (int index = 0; index < 2; index++) {
new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println(threadLocal.get().incrementAndGet()
+" thread:"+Thread.currentThread().getName());
}
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
运行结果
创建 AtomicInteger(952204834) thread:Thread-0
创建 AtomicInteger(471787139) thread:Thread-1
1 thread:Thread-0
2 thread:Thread-0
3 thread:Thread-0
4 thread:Thread-0
1 thread:Thread-1
5 thread:Thread-0
2 thread:Thread-1
3 thread:Thread-1
4 thread:Thread-1
5 thread:Thread-1
从运行结果可以得出每个线程都创建了
AtomicInteger实例,因此彼此不会产生影响。
ThreadLocal<T> 可以看出两部分:
- 一个是
ThreadLocal对象实例(即例子中的threadLocal),这个实例只有一个,多线程共享的。 - 另一个是由
ThreadLocal对象实例创建的对象(即例子中的AtomicInteger),这个是每个线程都会创建并持有。
因此你会发现:
- 每个线程可以根据
ThreadLocal对象实例threadLocal来查找对应的所创建的对象AtomicInteger,相当于key->value的键值映射关系。- 而每个线程可以有多个
ThreadLocal对象实例,即多个key。- 那么我们可以断定,每个线程肯定有一个集合对象来存储上面的多个
key->value键值映射关系,其实就是Thread中成员属性ThreadLocal.ThreadLocalMap threadLocals。
1.2 get 方法
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
public T get() {
// 先获取当前线程
Thread t = Thread.currentThread();
// 从当前线程中获取存储键值映射关系的Map
ThreadLocal.ThreadLocalMap map = getMap(t);
if (map != null) {
// 如果这个Map存在,那么直接从里面获取映射关系e
ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
// 映射关系e 存在,那么直接获取创建的对象
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 程序走到这里,说明当前线程 这个ThreadLocal 实例对应对象还没有创建,
// 那么就进行初始化创建
return setInitialValue();
}
从当前线程存储的映射关系集合
threadLocals中,查找当前这个ThreadLocal对象实例所对应的对象是否存在;存在就返回,不存在就setInitialValue()方法进行创建。
1.3 setInitialValue() 方法
private T setInitialValue() {
// 调用 initialValue() 方法得到初始化值
T value = initialValue();
// 先获取当前线程
Thread t = Thread.currentThread();
// 从当前线程中获取存储键值映射关系的Map
ThreadLocal.ThreadLocalMap map = getMap(t);
if (map != null)
// 存储 key-value 的映射关系
map.set(this, value);
else
createMap(t, value);
return value;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
1.4 ThreadLocalMap
ThreadLocalMap也是用一个哈希表数据结构来储存key-value 的映射关系,只不过它不是用链地址法来解决哈希冲突,而是用开放地址法的 线性探测来解决哈希冲突。
关于哈希表,以及链地址法和开放地址法的原理,在我的这篇文章哈希表 中有全面的介绍。
1.5 小结

从图中我们就可以知道,每个线程中都一个
threadLocals属性,它的类型是ThreadLocalMap, 这个ThreadLocalMap会记录当前线程所有产生的ThreadLocal对象。
二. FastThreadLocal<V>
private static final FastThreadLocal<AtomicInteger> fastThreadLocal = new FastThreadLocal<AtomicInteger>(){
@Override
protected AtomicInteger initialValue() {
AtomicInteger result = new AtomicInteger(0);
System.out.println("创建 AtomicInteger("+result.hashCode()+") thread:"+Thread.currentThread().getName());
return result;
}
};
public static void main(String[] args) {
for (int index = 0; index < 2; index++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(fastThreadLocal.get().incrementAndGet()
+" thread:"+Thread.currentThread().getName());
}
}
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
运行结果
创建 AtomicInteger(125165235) thread:Thread-1
创建 AtomicInteger(1223947416) thread:Thread-0
1 thread:Thread-1
1 thread:Thread-0
2 thread:Thread-1
2 thread:Thread-0
3 thread:Thread-1
3 thread:Thread-0
4 thread:Thread-1
5 thread:Thread-1
4 thread:Thread-0
5 thread:Thread-0
从运行结果来看,FastThreadLocal<V> 和 ThreadLocal<T> 效果是一样的,那么 FastThreadLocal<V> 的优点在哪里呢?
从上面的介绍中,我们知道 ThreadLocal<T> 通过哈希表来储存数据,从哈希表查找数据的过程如下:
- 根据
ThreadLocal<T>实例对象threadLocal的哈希值,得到对应数组下标。 - 再比较这个数组下标存储的映射关系
entry的key和实例对象threadLocal是否相等,相等的话,就直接返回entry的value值。 - 如果不等,那么就要进行线性探测,查找下一个下标的映射关系
entry,是否符合要求,知道找到映射关系entry的key和当前实例对象threadLocal相等。 - 所以对于这种开发地址法的哈希表,极个别情况下,查找过程可能会耗时,要进行多次线性探测。
那么 FastThreadLocal<V> 就采用了空间换时间的方式加快查找速度。
-
ThreadLocal<T>VSFastThreadLocal<V>-
ThreadLocal<T>有一个threadLocalHashCode属性,在创建的时候被赋值,而且是不可变的属性,代表当前这个ThreadLocal<T>实例对象的哈希值,用来在哈希表ThreadLocalMap中查找对应的映射关系。 -
FastThreadLocal<V>有一个index属性,在创建的时候被赋值,而且是不可变的属性,这个值就代表当前FastThreadLocal<V>实例对象在InternalThreadLocalMap实例的indexedVariables的下标,通过这个下标得到FastThreadLocal<V>所创建的当前线程对象。
-
-
ThreadLocalMap和InternalThreadLocalMap-
ThreadLocalMap是一个哈希表,用来储存ThreadLocal<T>实例对象和它所创建的当前线程对象的映射关系,就可以通过ThreadLocal<T>实例对象查找它所创建的当前线程对象。 -
InternalThreadLocalMap就是一个数组,用来存储FastThreadLocal<V>实例对象所创建的当前线程对象,不过存储这个值的数组下标就是FastThreadLocal<V>实例对象的index属性值。 -
InternalThreadLocalMap数组下标0这个位置比较特殊,0下标存储当前线程所有的FastThreadLocal<V>对象实例,用于当前线程销毁时,移除当前线程所有的FastThreadLocal<V>对象实例所创建的当前线程对象。
-

需要注意的点:
- 每个
FastThreadLocal再创建的时候,index属性就被赋值了,也就是说这个FastThreadLocal实例,在每个线程获取的InternalThreadLocalMap中的下标是一样的,都是index。这就导致一个严重问题,如果
FastThreadLocal实例较多的话,某一个线程用到了index较大FastThreadLocal实例的话,它必须创建一个很大的数组,这样才能在FastThreadLocal实例对应下标index中储存FastThreadLocal实例创建的对象。 -
ThreadLocal没有这个问题,虽然它的哈希值也是创建的时候就确定了,但是它通过 哈希的方法寻找数组下标,那么当前线程中ThreadLocalMap的数组长度只会和当前线程拥有的ThreadLocal实例有关。这个的问题就是通过哈希查找,效率有点影响。
-
InternalThreadLocalMap的0下标做了特殊处理,用来存放每个线程拥有的FastThreadLocal实例集合,当线程退出时,清理这些FastThreadLocal实例为当前线程中产生的对象。 -
ThreadLocalMap没有做这方面的处理,那是因为ThreadLocalMap中使用WeakReference<ThreadLocal<?>>来记录ThreadLocal<?>实例的。