ThreadLocal最全最详细的总结
一、ThreadLocal是什么?
ThreadLocal,也就是线程本地变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。
1)创建
创建了一个ThreadLocal变量localVariable,任何一个线程都能并发访问localVariable。
//创建一个ThreadLocal变量
public static ThreadLocal<String> localVariable = new ThreadLocal<>();
2)写入
线程可以在任何地方使用localVariable,写入变量。
localVariable.set("曼巴”);
3)读取
线程在任何地方读取的都是它写入的变量。
localVariable.get();
二、你在工作中用到过ThreadLocal吗?
ThreadLocal的可使用场景有很多,如下:
- Web应用中的用户身份认证:
在 Web 应用中,可以使用ThreadLocal
存储当前用户的身份信息,这样在整个请求处理过程中都可以方便地访问用户的身份信息,而不必传递参数。 - 数据库连接管理:
在多线程环境下,每个线程可能需要访问数据库,并且通常使用连接池来管理数据库连接。可以使用ThreadLocal
存储数据库连接对象,保证每个线程都能获取到自己的连接,而不会与其他线程发生冲突。 - 事务管理:
在某些情况下,需要在多个方法调用之间共享事务上下文。可以使用ThreadLocal
存储事务对象,在同一个线程内的方法调用中共享事务对象,从而实现事务的传播和管理。 - 线程安全的日期格式化:
SimpleDateFormat
是线程不安全的类,如果多个线程同时访问一个SimpleDateFormat
实例,可能会导致格式化错误。可以使用ThreadLocal
创建线程局部的SimpleDateFormat
实例,确保每个线程都有自己的实例,从而避免线程安全问题。 - 跟踪日志:
在多线程环境下,需要在日志中记录每个线程的执行情况。可以使用ThreadLocal
存储日志上下文信息,如请求ID、用户ID等,确保每个线程的日志都能正确地记录上下文信息。
三、ThreadLocal怎么实现的呢?
我们看一下ThreadLocal的set(T)方法,发现先获取到当前线程,再获取ThreadLocalMap
,然后把元素存到这个map中。
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取ThreadLocalMap
ThreadLocalMap map = getMap(t);
//讲当前元素存入map
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocal实现的秘密都在这个ThreadLocalMap
了,可以Thread类中定义了一个类型为ThreadLocal.ThreadLocalMap
的成员变量threadLocals
。
public class Thread implements Runnable {
//ThreadLocal.ThreadLocalMap是Thread的属性
ThreadLocal.ThreadLocalMap threadLocals = null;
}
ThreadLocalMap既然被称为Map,那么毫无疑问它是<key,value>型的数据结构。我们都知道map的本质是一个个<key,value>形式的节点组成的数组,那ThreadLocalMap的节点是什么样的呢?
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
//节点类
Entry(ThreadLocal<?> k, Object v) {
//key赋值
super(k);
//value赋值
value = v;
}
}
这里的节点,key可以简单低视作ThreadLocal,value为代码中放入的值,当然实际上key并不是ThreadLocal本身,而是它的一个弱引用,可以看到Entry的key继承了 WeakReference(弱引用),再来看一下key怎么赋值的:
public WeakReference(T referent) {
super(referent);
}
key的赋值,使用的是WeakReference的赋值。
所以,怎么回答ThreadLocal原理?要答出这几个点:
1)Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,每个线程都有一个属于自己的ThreadLocalMap。
2) ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal的弱引用,value是ThreadLocal的泛型值。
3)每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
4)ThreadLocal本身不存储值,它只是作为一个key来让线程往ThreadLocalMap里存取值。
四、ThreadLocal 内存泄露是怎么回事?
我们先来分析一下使用ThreadLocal时的内存,我们都知道,在JVM中,栈内存线程私有,存储了对象的引用,堆内存线程共享,存储了对象实例。
所以呢,栈中存储了ThreadLocal、Thread的引用,堆中存储了它们的具体实例。
ThreadLocalMap中使用的 key 为 ThreadLocal 的弱引用。
“弱引用:只要垃圾回收机制一运行,不管JVM的内存空间是否充足,都会回收该对象占用的内存。”
那么现在问题就来了,弱引用很容易被回收,如果ThreadLocal(ThreadLocalMap的Key)被垃圾回收器回收了,但是ThreadLocalMap生命周期和Thread是一样的,它这时候如果不被回收,就会出现这种情况:ThreadLocalMap的key没了,value还在,这就会造成了内存泄漏问题。
那怎么解决内存泄漏问题呢?
很简单,使用完ThreadLocal后,及时调用remove()方法释放内存空间。
ThreadLocal<String> localVariable = new ThreadLocal();
try {
localVariable.set("曼巴”);
……
} finally {
localVariable.remove();
}
那为什么key还要设计成弱引用?
key设计成弱引用同样是为了防止内存泄漏。
假如key被设计成强引用,如果ThreadLocal Reference被销毁,此时它指向ThreadLoca的强引用就没有了,但是此时key还强引用指向ThreadLocal,就会导致ThreadLocal不能被回收,这时候就发生了内存泄漏的问题。
五、ThreadLocalMap的结构了解吗?
ThreadLocalMap虽然被叫做Map,其实它是没有实现Map接口的,但是结构还是和HashMap比较类似的,主要关注的是两个要素:元素数组
和散列方法
。
-
元素数组
一个table数组,存储Entry类型的元素,Entry是ThreaLocal弱引用作为key,Object作为value的结构。
private Entry[] table;
-
散列方法
散列方法就是怎么把对应的key映射到table数组的相应下标,ThreadLocalMap用的是哈希取余法,取出key的threadLocalHashCode,然后和table数组长度减一&运算(相当于取余)。
int i = key.threadLocalHashCode & (table.length - 1);
这里的threadLocalHashCode计算有点东西,每创建一个ThreadLocal对象,它就会新增0x61c88647
,这个值很特殊,它是斐波那契数 也叫 黄金分割数。hash
增量为 这个数字,带来的好处就是 hash
分布非常均匀。
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
六、ThreadLocalMap怎么解决Hash冲突的?
我们可能都知道HashMap使用了链表来解决冲突,也就是所谓的链地址法。
ThreadLocalMap没有使用链表,自然也不是用链地址法来解决冲突了,它用的是另外一种方式——开放定址法。开放定址法是什么意思呢?简单来说,就是这个坑被人占了,那就接着去找空着的坑。
如上图所示,如果我们插入一个value=5的数据,通过 hash计算后应该落入第 5 个槽位中,而槽位 5 已经有了 Entry数据,而且Entry数据的key和当前不相等。此时就会线性向后查找,一直找到 Entry为 null的槽位才会停止查找,把元素放到空的槽中。
在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该槽位Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置。
七、ThreadLocalMap扩容机制了解吗?
从上面得知,ThreadLocal的初始大小是16。那么问题来了,ThreadLocal是如何扩容的?
在set方法中会调用rehash方法:
private void set(ThreadLocal<?> key, Object value) {
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();
}
注意一下,其中有个判断条件是:sz(之前的size+1)如果大于或等于threshold的话,则调用rehash方法。
threshold默认是0,在创建ThreadLocalMap时,调用它的构造方法:
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)
{
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
调用setThreshold方法给threshold设置一个值,而这个值INITIAL_CAPACITY是默认的大小16。
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
也就是第一次设置的threshold = 16 * 2 / 3, 取整后的值是:10。
换句话说当sz大于等于10时,就可以考虑扩容了。
rehash代码如下:
private void rehash() {
//先尝试回收一次key为null的值,释放出一些空间
expungeStaleEntries();
if (size >= threshold - threshold / 4)
resize();
}
在真正扩容之前,先尝试回收一次key为null的值,腾出一些空间。
如果回收之后的size大于等于threshold的3/4时,才需要真正的扩容。
计算公式如下:
16 * 2 * 4 / 3 * 4 - 16 * 2 / 3 * 4 = 8
也就是说添加数据后,新的size大于等于老size的1/2时,才需要扩容。
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
//按2倍的大小扩容
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
resize中每次都是按2倍的大小扩容。
扩容的过程如下图所示:
扩容的关键步骤如下:
老size + 1 = 新size
如果新size大于等于老size的2/3时,需要考虑扩容。
扩容前先尝试回收一次key为null的值,腾出一些空间。
如果回收之后发现size还是大于等于老size的1/2时,才需要真正的扩容。
每次都是按2倍的大小扩容。
八、父子线程怎么共享数据?
父线程能用ThreadLocal来给子线程传值吗?答案是:不能。那该怎么办?
这时候可以用到另外一个类——InheritableThreadLocal
。
使用起来很简单,在主线程的InheritableThreadLocal实例设置值,在子线程中就可以拿到了。
public class InheritableThreadLocalTest {
public static void main(String[] args) {
final ThreadLocal threadLocal = new InheritableThreadLocal();
// 主线程
threadLocal.set("不擅打篮球");
//子线程
Thread t = new Thread() {
@Override
public void run() {
super.run();
System.out.println("曼巴 ," + threadLocal.get());
}
};
t.start();
}
}
那原理是什么呢?
原理很简单,在Thread类里还有另外一个变量:
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
在Thread.init的时候,如果父线程的inheritableThreadLocals
不为空,就把它赋给当前线程(子线程)的inheritableThreadLocals
。
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);