原文链接:https://www.jylt.cc/#/detail?activityIndex=2&id=9df3fd62d6ee13ff555c30157798b092
ThreadLocal用来提供线程内部的局部变量,是各个线程独有的,该变量无法被其他线程访问。主要有以下作用:
其中最典型的应用是对数据库连接池的处理。可以参考这篇文章阅读:ThreadLocal在数据库连接中的应用
ThreadLocal主要有以下几个方法:
public void set(T value); // 存值
public T get(); // 取值
public void remove(); // 移除线程局部变量的引用
多线程的时候 ThreadLocal
为什么能够做到线程数据的隔离呢?原因是由于每个线程Thread都维护了一个 ThreadLocal.ThreadLocalMap
的引用,而 ThreadLocalMap
就是存放 ThreadLocal
值的地方。引用关系如下图:
下面根据源码来解释以下上面的引用关系。
public class Thread implements Runnable {
// Thread拥有ThreadLocal.ThreadLocalMap的引用
ThreadLocal.ThreadLocalMap threadLocals = null
}
注意上图的红线部分和 Entry extends WeakReference
。Entry的key被包装成了弱引用是什么原因呢?
首先要知道弱引用的作用,我们都知道平时我们创建对象 Object o = new Object()
,这种方式是强引用,在对象 o
使用完之前,该对象是不会被垃圾回收的,因为该对象是可达状态;该对象使用完之后,是可以被垃圾回收的,因为该对象是不可达的。如果 o
是弱引用对象,并且没有其他强引用对象对其引用时,不管任何收执行GC,对象 o
都会被垃圾回收掉。
可以看出红线部分的设计是为了防止key长时间无法被GC,导致内存溢出。
public class ThreadLocal<T> {
// ThreadLocal的内部类,这个也就是上面Thread里面持有的threadLocals对象
static class ThreadLocalMap {
// Entry是具体存放ThreadLocal数据的容器,可以发现Entry的数据结构跟Hash Map的是比较像的,都是形式。此处的Entry的key是ThreadLocal对象,下面会说到
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// table是Entry数组的原因是,每个线程可能会有多个ThreadLocal对象,这个时候,需要将不同ThreadLocal对象对应的值放到不同下标的Entry数组中,具体如何存放的下面会说到
private Entry[] table;
}
}
该方法是向ThreadLocal中存放值的,如下:
ThreadLocal<Integer> tl = new ThreadLocal<>();
tl.set(1);
具体设置值的逻辑:
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取当前线程引用的 threadLocals 对象
ThreadLocalMap map = getMap(t);
if (map != null)
// 设置值
map.set(this, value);
else
// 创建新的引用
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
// 获取当前线程持有的threadLocals
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
// 初始化一个 ThreadLocalMap 赋值给当前线程的 threadLocals
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 计算数据应该存放的位置
int i = key.threadLocalHashCode & (len-1);
// 在进行设置值的时候,为了解决hash冲突,使用了 线性探测法
// 如果第 i 个位置已经有值了,则判断下一个位置有没有值,没有值则将数据放入该位置
// 整个循环的意思是,从上面获取的hash下标开始向后遍历,在遍历过程中如果当前下标的Entry没有值,如果有值,判断Entry的key是不是当前threadLocal对象,如果是,则给当前ThreadLocal设置新的value;如果Entry的key为空,说明该Entry已经没有引用的ThreadLoca了,无法再被访问到,将该无效Entry移除,然后赋值新的key和value
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) {
// 说明当前的 threadLocal 对象已经被GC清理,移除失效的 Entry,下面会说到
replaceStaleEntry(key, value, i);
return;
}
}
// 说明当前下标的Entry还没有值,初始化一个新Entry
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// map.getEntry 通过循环遍历的方式查找当前 ThreadLocal
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
// Entry的key==当前threadLocal,说明是要查询的Entry
return e;
else
// 通过线性探测法,循环获取下一个下标的Entry,并判断是不是目标Entry
return getEntryAfterMiss(key, i, e);
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
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) {
// 移除 key 的引用
e.clear();
// 移除 value 的引用
expungeStaleEntry(i);
return;
}
}
}
使用完 ThreadLocal 之后, 一定要手动调用 remove 方法 ,不然可能会导致内溢出。前面说了 Entry 里的 key 是弱引用对象,可以避免了内存溢出。但是 value 是强引用对象,如果 value 的对象还被其他对象引用,value 会一直不被 GC 回收,如果这样的 value 比较多的时候,会导致内存溢出。
value可能被长时间引用的原因是Thread的生命周期要比对象的生命周期长的多,在整个生命周期内,可能会创建了许许多多的ThradLocal,这时value对象就会特别多,而且不会被垃圾回收。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 将当前 Entry 的 value 置为 null
tab[staleSlot].value = null;
// 将当前 Entry 置为 null
tab[staleSlot] = null;
// Entry 数量 -1
size--;
Entry e;
int i;
// 通过线性探测法将 table 中所有失效的 Entry 都做清理
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
如果在线程中创建了子线程,那么子线程与父线程的 ThreadLocal 数据是不能共享的,比如下面的代码:
public static void main(String[] args) {
ThreadLocal<Integer> local = new ThreadLocal<>();
local.set(1);
System.out.println("父线程get=" + local.get());
new Thread(() -> {
System.out.println("子线程get=" + local.get());
}).start();
}
// 输出结果:
// 父线程get=1
// 子线程get=null
如何在子线程中使用父线程 ThreadLocal 数据呢?可以使用 InheritableThreadLocal
,如下代码:
InheritableThreadLocal<Integer> local1 = new InheritableThreadLocal<>();
local1.set(1);
System.out.println("父线程get1=" + local1.get());
new Thread(() -> {
System.out.println("子线程get1=" + local1.get());
}).start();
// 打印结果
// 父线程get1=1
// 子线程get1=1
其原理是因为在调用 Thread 的构造方法的时候,会将父线程的局部变量赋值给子线程,实现了在子线程能够使用到父线程数据。
但这种方法不能在线程池中使用,线程池中的线程不一定是当前线程创建的。
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
init(g, target, name, stackSize, null, true);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
Thread parent = currentThread();
// parent.inheritableThreadLocals 的值是在调用 set 方法时设置的
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
// 将父线程的局部变量赋值给子线程
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
}
可以参考阿里的开源项目:Gitee 极速下载/transmittable-thread-local