ThreadLocal
【并发设计模式】聊聊线程本地存储模式如何实现的线程安全
【Java并发】从simpleDateFormart聊聊threadlocal原理机制
前两篇中已经从源码角度进行剖析,本篇主要从设计角度剖析。
在并发中为了保证数据安全,可以采用数据隔离的方式 也就是大家都自己内部维护一份数据,在实际落地的时候,其实就是针对每个线程内部持有自己特殊的数据。ThreadLocal就是一个案例。
那么我们思考下,如果你是JDK的设计者你会如何设计,一般就是使用一个全局Map<Thread,具体数据>,显然是用这种方式,在写操作的时候,需要进行加锁,无论是lock 还是CAS的方式,都存在一定的性能损耗。
那么ThreadLocal是如何做的。因为数据是对于线程级别的,所以对于Thread来说,内部持有一个ThreadLocalMap,而这个就是一个Entry数组,Key是ThreadLocal,Value是具体数据。
优秀设计
线程内数据隔离,避免锁竞争
ThreadLocal巧妙使用内部持有一个私有的ThreadLocalMap来存储数据。可以避免锁竞争。
易用性,封装复杂性
在使用的时候,其实对于程序员来说,不需要了解具体的内部细节,直接操作ThreadLocal的set get就可以获取数据,使用起来比较好用,封装内部复杂性。
弱引用避免内存泄漏
其实主要就是两点 一个依赖于Key是软引用,当ThreadLocal外部不在使用的时候,就会进行下次GC的时候,回收,但是对应的value还存在,如何避免,那就是使用remove() , 当前 本身 get \ set 也会清除key=null 的数据。
使用线性探测法,而不是拉链法
int i = key.threadLocalHashCode & (len-1);
在进行数据set的时候,使用的hash,那么有hash一定会存在hash冲突,ThreadLocalMap是如何解决的,答案是下探法。
一句话就是,如果当前位置有数据,那么就延续后一个位置,如果都满了,在进行扩容操作。
那么为什么ThreadLocalMap要这么设计,其实主要就是两点,第一个本身 时间效率存储的数据不多,并且ThreadLocalMap是一个数组,可以有效的利用内存数据的连续性 内存的预读性, 高效的查找和存储数据,第二点,空间效率,如果采用拉链法,那么就需要多存储一个next指针,占用一定的空间。
既然ThreadLocal已经这么优秀了,那么为什么还有Netty实现一个呢,答案其实就只要应对的场景不同,ThreadLocal本身只是提供一些少量数据的存储,对于支持高并发来说,极端情况下,线性探测法时间复杂度在O(N),以及在数据满之后,要进行的Rehash操作。当如还有内存泄漏的问题。
FastThreadLocal
最佳实践
具体使用其实就是通过创建FastThreadLocalThread 然后通过 FastThreadLocal 设置值。
final FastThreadLocal fastThreadLocal = new FastThreadLocal();
final FastThreadLocal fastThreadLocal2 = new FastThreadLocal();
final FastThreadLocal fastThreadLocal3 = new FastThreadLocal();
FastThreadLocalThread fastThreadLocalThread = new FastThreadLocalThread(()-> {
fastThreadLocal.set("1");
fastThreadLocal.get();
fastThreadLocal2.set("2");
fastThreadLocal2.get();
fastThreadLocal3.set("3");
fastThreadLocal3.get();
Thread thread = Thread.currentThread();
System.out.println(thread);
},"T1");
fastThreadLocalThread.start();
源码解析
初始化
FastThreadLocalThread的初始化,因为内部持有一个threadLocalMap引用。
public class FastThreadLocalThread extends Thread {
private InternalThreadLocalMap threadLocalMap;
}
当我们创建一个FastThreadLocal对象的时候,会通过全局的InternalThreadLocalMap#nextVariableIndex
方法创建一个全局唯一自增ID,CAS的方式进行获取。比如当前获取的index=1
public FastThreadLocal() {
// 创建一个全局的唯一下标记
index = InternalThreadLocalMap.nextVariableIndex();
}
public static int nextVariableIndex() {
int index = nextIndex.getAndIncrement();
return index;
}
Set
当设置一个值的时候,先判断当前值是否是默认值,如果不是那么进行设置,如果是的话进行执行remove()的逻辑。
1.先获取InternalThreadLocalMap
2.设置对应的值
public final void set(V value) {
// value = unset 进行remove
if (value != InternalThreadLocalMap.UNSET) {
// 获取当前线程的InternalThreadLocalMap
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
setKnownNotUnset(threadLocalMap, value);
} else {
// value是Object 初始化
// 把当前FastThreadLocal的操作 重置
// 1.对应的位置的值 设置成UNSET
// 2.set集合中里面的FastThreadLocal 删除
remove();
}
}
1.先获取当前线程
2.判断当前是否是FastThreadLocalThread 或者普通Thread,进行不同的处理。
public static InternalThreadLocalMap get() {
Thread thread = Thread.currentThread();
// 当前是线程类型是FastThreadLocalThread
if (thread instanceof FastThreadLocalThread) {
// 直接从FastThreadLocalThread 获取internalThreadLocalMap
return fastGet((FastThreadLocalThread) thread);
} else {
// 普通的threadLocal 时机上很对于普通的thread 也可以使用internalThreadLocalMap
return slowGet();
}
}
1.从当前线程获取,如果为空,直接new一个 并进行赋值操作。
private static InternalThreadLocalMap fastGet(FastThreadLocalThread thread) {
InternalThreadLocalMap threadLocalMap = thread.threadLocalMap();
if (threadLocalMap == null) {
thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap());
}
return threadLocalMap;
}
设置值
/**
* 存储数据
* 按照index 对应数组的下表存储数据
* @see InternalThreadLocalMap#setIndexedVariable(int, Object).
*/
private void setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) {
// 按照index 存储进去
if (threadLocalMap.setIndexedVariable(index, value)) {
// 把fastThreadLocal对象 存储到set object[0]
addToVariablesToRemove(threadLocalMap, this);
}
}
这里其实就是先判断index是否超过数组范围,如果没有超过范围,那么就执行赋值,否则就需要扩容在赋值操作。
public boolean setIndexedVariable(int index, Object value) {
Object[] lookup = indexedVariables;
// 在范围内的
if (index < lookup.length) {
// 获取可能存在的老值
Object oldValue = lookup[index];
// 赋值操作
lookup[index] = value;
// 等于object
// true : 说明在这个位置 没有存过东西
// false : 说明这个位置 已经存在过别的数据 等于覆盖
return oldValue == UNSET;
} else {
// 扩容处理
// 扩容的标准 使用超过多少 -- index扩容
expandIndexedVariableTableAndSet(index, value);
return true;
}
}
这里是核心,需要维护添加新的数据,存储在0号位置。大概就是看0号位置有没有数据,没有创建一个Set 对象
private static void addToVariablesToRemove(InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {
Object v = threadLocalMap.indexedVariable(VARIABLES_TO_REMOVE_INDEX);
Set<FastThreadLocal<?>> variablesToRemove;
// 第一次进来 创建一个SET集合 Object数组的0号位置
if (v == InternalThreadLocalMap.UNSET || v == null) {
variablesToRemove = Collections.newSetFromMap(new IdentityHashMap<FastThreadLocal<?>, Boolean>());
// set 存储到0号位置
threadLocalMap.setIndexedVariable(VARIABLES_TO_REMOVE_INDEX, variablesToRemove);
} else {
// 拿到set集合
variablesToRemove = (Set<FastThreadLocal<?>>) v;
}
// 将fastThreadLocal 添加到set中
variablesToRemove.add(variable);
}
Get
获取数据比较简单, 其实直接通过index获取就可以。当然考虑到可能没有数据,initialize就是一个拓展兼容处理。
public final V get() {
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
Object v = threadLocalMap.indexedVariable(index);
if (v != InternalThreadLocalMap.UNSET) {
return (V) v;
}
return initialize(threadLocalMap);
}
优秀设计
快在哪里
空间换时间,ThreadLocal慢在线性探测,那么直接通过更大数组空间的开辟,避免线性探测,这是一种空间换时间的思想,而FastThreadLocal就是这么做的。通过数组的方式进行获取查找数据。O(1)的时间复杂度。