ThreadLocal(线程本地存储模式)
-
多线程之所以让人头疼就是其对于临界区即共享变量的改变是难以预计的。
但正所谓没有买卖就没有伤害,在多线程世界里没有共享就没有伤害。局部变量存储在栈里,栈是线程私有的,所以局部变量就不存在多线程的安全性问题,那么除此之外还有没有是线程安全的存储方式呢。 -
当然有,那就是ThreadLocal即线程本地存储。spring的事务控制,动态数据源自定义注解的数据源切换都用到了ThreadLocal,那我们就来看看ThreadLocal到底是为什么可以做到线程隔离。
ThreadLocal threadLocal = new ThreadLocal();
public void fun(String[] args) {
threadLocal.set("123");
final Object o = threadLocal.get();
}
大家都知道ThreadLocal对象拥有两个核心成员方法,set和get,同一个线程内使用同一个ThreadLocal对象set一个值,就能get出这个值,set第二遍就会覆盖上一个值。不同的线程不会get到其他线程set的数据。到底是什么样的数据结构可以让成员变量threadLocal保证线程之间不会互相影响的呢。
- 我们思考下,线程都有自己的一个唯一Id,我们假设将id作为key,
set(value)
的实际作用是set(currentThreadId,value)
,而get()
的实际实现是get(currentThreadId)
,这样子ThreadLocal内部就维护了一个Map表,如下图所示
- 它的核心方法实现是这样的,如下代码所示
/**
* @author pp_x
* @email pp_x12138@163.com
* @Description
* @date 2022/11/20 19:43
*/
public class MyThreadLocal<T> {
Map<Thread, T> locals =
new ConcurrentHashMap<>();
// 获取线程变量
T get() {
return locals.get(
Thread.currentThread());
}
// 设置线程变量
void set(T t) {
locals.put(
Thread.currentThread(), t);
}
}
这样子,将线程唯一id作为map的key,就完全可以实现不同线程存的值不会相互影响了,完美!收工!
jdk内部的ThreadLocal实现真的是这样的吗,当然不是,那么到底是什么样的呢,点进源码不就知道了,下面就是ThreadLocal在jdk8下的get方法和set方法的实现
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();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
根据源码可以看到,方法内部获取了类型为ThreadLocalMap的变量,但是不同的是这个map的key并不是线程id而是当前ThreadLocal对象(this)。这也就证明我们的思考是错误滴,下面是jdk对于ThreadLocal的实现概念图,这边贴出来便于大家进行对比
- 其实再往下看源码就会发现ThreadLocalMap这个变量并不是ThreadLocal内部持有的,而是Thread内部持有的,这是上面getMap()的内部实现,我们就能看出ThreadLocalMap的从属关系。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
- 那么这么也就是说ThreadLocal其实只是一个代理类,内部并不持有任何与线程相关的数据,所有和线程
相关的数据都存储在 Thread 里面。其实这样才是最合理的。当然这样设计的原因就是,不容易产生内存泄漏 - 我们回到我们上面的思考的模型,图再贴一下方便查看
- 每有一个线程调用set方法,就会往ThreadLocal内部的map里插入一条数据,而map是ThreadLocal的强引用,只要ThreadLocal对象存在,map就永远不会被回收,即使线程销毁了,map的内存空间依然不会释放,那么大量线程频繁调用set,自然就产生内存泄漏了。
- 反观jdk内部的结构,我们会发现ThreadLocalMap是ThreadLocal的静态内部类,同时也是Thread的成员变量。我们每使用一次
threadLocal.set(value)
,就会获取当前线程内部的ThreadLocalMap,并将当前threadLocal的对象引用作为key存入map,这样的好处是,ThreadLocalMap的生命周期和Thread强关联,并且ThreadLocalMap里对于ThreadLocal的引用还是弱引用(WeakReference),当线程销毁后map的空间自然释放了。
ThreadLocalMap的内部实现
- 通过上面ThreadLocalMap的代码可以看到,其内部还持有一个Entry的静态内部类,继承了ThreadLocal的弱引用,Entry仅拥有属性
value
,通过Entry的构造方法可以看出,Entry中的这个value
即存放ThreadLocal.set(value)
的value值。 - ThreadLocalMap中持有Entry类的数组对象table,table内存存放的就是同一个线程使用多个ThreadLocal对象set进来的值。
ThreadLocal的核心方法调用流程
Set方法调用流程
- 调用ThreadLocal对象的set后,通过Thread类的currentThread()方法获取当前线程,并获取当前线程的ThreadLocalMap对象,再调用ThreadLocalMap对象的set方法,下方是ThreadLocalMap的set方法的实现具体逻辑可以自行翻阅源码
- 大致是根据ThreadLocal引用key的hash和数组长度确定当前索引,取出索引对应的Entry,再获取Entry的ThreadLocal的引用key,最后做一系列判断操作进行value的赋值
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
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;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
get方法调用流程
- 调用ThreadLocal对象的get后,同set一样最后调用ThreadLocalMap的getEntry方法,取出Entry内部的value对象。下面是ThreadLocalMap内部的genEntry方法实现
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
总结
- 以上是本人学习ThreadLocal后的理解,如有不对,希望可以如数指出!