ThreadLocal是多线程情况下常用的一种线程隔离的变量,今天我们就来了解一下它的底层是如何实现的
目录
一. 简介
ThreadLocal,也称线程变量,每个ThreadLocal对象存储的变量仅仅属于当前线程,对于其他线程而言,无法获取到该变量,是一种线程隔离的变量,适用于一些在每个线程的上下文中,需要频繁地获取该变量,但处于需求或隐私考虑,需要独属于该线程的情况。
作用:
-
提供线程独立性:每个线程可以拥有自己的变量副本,不会受到其他线程的影响,确保数据独立和安全。
-
避免线程同步:通过
ThreadLocal
,可以避免使用锁机制来处理线程间的数据共享问题,从而简化并发编程,提升程序的性能。 -
简化线程上下文传递:在多线程环境中,
ThreadLocal
可以隐式地存储线程特有的数据,避免在方法调用中显式传递这些数据,尤其在复杂的调用链中非常实用。 -
维护线程状态:
ThreadLocal
可以用于保存与线程生命周期相关的状态信息,例如当前线程处理的事务、数据库连接、用户信息等
二. 基本使用方式
ThreadLocal支持传入泛型
public class Main {
static ThreadLocal threadLocal = new ThreadLocal();
static ThreadLocal<String> threadLocal2 = new ThreadLocal();
public static void main(String[] args) {
threadLocal.set(1);
System.out.println(threadLocal.get());
threadLocal2.set("B");
System.out.println(threadLocal2.get());
//演示线程隔离
new Thread(()->{
System.out.println(threadLocal.get());
}).start();
}
}
运行结果:
三. 原理解析
3.1 ThreadLocalMap
ThreadLocal的线程独立性底层是基于ThreadLocal的一个静态内部类ThreadLocalMap实现的,首先让我们粗略了解一下ThreadLocalMap:
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* .......
*/
}
可以看到的是ThreadLocalMap的每个节点的key值,即ThreadLocal对象是弱引用,也就是说会在每次gc的时候进行回收,这可能是设计者想要避免OOM问题所使用的特殊设计。
但是大家有没有发现一个问题;value值仍然是强引用,gc的时候不会回收
也就是说,如果在程序运行情况下,程序内存中会累积一个又一个线程的ThreadLocalMap的value值的垃圾信息,那么总有一天也会出现OOM问题!
所以虽然设计者考虑到了使用ThreadLocal时尽量避免OOM问题,但没有完全解决,仍然需要我们每次在使用完成后,调用threadLocal.remove()方法:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
static class ThreadLocalMap {
//...
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
//...
}
remove()方法会将ThreadLocalMap所有节点设置为null,这样就能在gc的时候被回收了。
另外我们可以看到ThreadLocalMap中有一个Entry数组,但是与HashMap相似相非
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;
//重新散列与删除key值为null的结点
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
相同点:ThreadLocalMap和HashMap一样,会先对value值进行哈希,计算出value值将要落在数组内的坐标。
不同点:HashMap采用拉链法/红黑树解决哈希冲突,而ThreadLocalMap采用向后寻找空余位的方式解决哈希冲突。
总结一下,ThreadLocalMap是一个ThreadLocalMap的静态内部类,可以看作一个key值限定为ThreadLocal,做了些特殊处理的哈希表。
3.2 get()方法
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;
}
}
//初始化ThreadLocalMap
return setInitialValue();
}
static class ThreadLocalMap {
//......
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//......
}
可以看到ThreadLocal的get()方法,首先从当前线程Thread获取一个ThreadLocalMap对象引用,然后在该ThreadLocalMap哈希表中搜寻key值对应的value值返回。
public class Thread implements Runnable {
//...
ThreadLocal.ThreadLocalMap threadLocals = null;
//...
}
threadLocals是Thread中的一个非静态属性,也就是说它是每个Thread独有的一个引用,每次初始化threadLocals会在堆区开辟一个新的ThreadLocalMap对象
3.3 set()方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
static class ThreadLocalMap {
//......
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)]) {
if (e.refersTo(key)) {
e.value = value;
return;
}
if (e.refersTo(null)) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
//重新散列与删除key值为null的结点
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
//......
}
与get()方法一致,threadLocal会通过调用Thread.currentThread()获取当前Thread对象,再通过getMap(t)获取当前线程指向的ThreadLocalMap,并尝试像哈希表一样插入key和value
3.4 架构图
厘清一下以上分析的脉络,我们可以得到下图:
threadLocal:堆区开辟的ThreadLocal对象的一个引用
ThreadLocal:堆区开辟的对象
Thread.currentThread():native修饰的本地方法,返回当前线程Thread对象
Thread对象:threadLocals属性默认为null,第一次使用threadLocal.set()方法后在堆区新开辟一个ThreadLocalMap对象
ThreadLocalMap:使用ThreadLocal作为key值,传入值作为value值
也就是说在多线程情况下如下图所示:
尽管threadLocal引用的是堆区中的同一个ThreadLocal对象,但是由于ThreadLocalMap不同,那么存储与取出的value值互不影响,实现线程独立性。
本篇文章到此结束,希望能对大家有所帮助