在平常开发的时候,经常使用到线程本地变量,这种类型的变量会在每个线程中都有一份,互相不会产生影响,这样来解决多线程并发问题。
那么是如何实现的呢?
一. 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 小结
ThreadLocal.png从图中我们就可以知道,每个线程中都一个
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<?>
实例的。
网友评论