介绍
ThreadLocal中设置的值仅属于当前线程,该值对其他线程而言是隔离的,所以在同一时间并发修改一个属性的值也不会互相影响。
使用
在使用ThreadLocal时,可以直接通过
set(T value)
、 get()
来设置threadLocal的值、获取threadLocal的值。
set方法
public void set(T value) {
Thread t = Thread.currentThread(); // 获取当前线程
ThreadLocalMap map = getMap(t); // 获取当前线程的ThreadLocalMap
if (map != null) { // 如果map不是空
map.set(this, value); // 设置值
} else {
createMap(t, value); // 创建并设置值
}
}
// 获取线程的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 对该ThreadLocal设置值
private void set(ThreadLocal<?> key, Object value) {
// ThreadLocalMap内部的table数组
Entry[] tab = table;
int len = tab.length;
// 根据threadLocal的hash和长度进行与运算,找到下标位置
int i = key.threadLocalHashCode & (len-1);
// 曾经该threadLocal有值,设置值并返回
for (Entry e = tab[i];e != null; e = tab[i = nextIndex(i, len)]) {
// 获取entry的引用
ThreadLocal<?> k = e.get();
// 引用等于当前threadLocal 则进行设置值
if (k == key) {
e.value = value;
return;
}
// 当前引用为空,把key、value组装成entry放到i位置上,并清楚key为空的entry
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 组装entry
tab[i] = new Entry(key, value);
int sz = ++size;
// 如果没有元素被清楚,并当前数组大小大于threshold则进行rehash;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
其中 threshold = len * 2 / 3,它是通过setThreshold
方法进行设置的。而每次rehash
的时候都会调用resize
方法,它会读取oldTable.length,把newLen
设置为oldLen
的两倍。这里有一个注意点int i = key.threadLocalHashCode & (len-1);
下标是通过hash来确定的,会出现hash冲突,这里采用的是开放地址法来解决hash冲突,在下面的代码中有判断k==key
,如果不相等则nextIndex(i, len)
获取下一个下标来判断。
上述就是整个set
的过程,下面来看一下get
public T get() {
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// this为当前threadLocal,获取对应的entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
// 返回当前entry的值即可。
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 设置初始值并返回,初始值是null
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进行返回
return e;
else
// 开始遍历entry数组,如果能找到key的entry就返回否则返回null
return getEntryAfterMiss(key, i, e);
}
get
方法要比set
简单很多,只是根据key找对应entry,把entry的值返回即可。
结构
通过上述源码,可以总结出threadLocal的数据结构如下:
问题
根据上面的介绍,可以看出一些潜在的问题;例如在使用threadLocal时堆栈信息如下:
真的会内存泄漏?
当使用完threadLocal,threadLocal的对象引用就不存在了,而key对threadLocal是弱引用,gc后这段引用也不存在了。此时无法通过map.getEntry(this)
找到对应的entry,而entry还一直存在Entry[]中,就有可能
导致了内存溢出。
这里我写了是有可能导致内存溢出,例如在set
方法中有这样一行代码
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
该方法的具体代码如下:
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
当有新的threadlocal进行设置值时都会进行清除一下e.get() == null
引用为空的Entry,而进入到这里的条件是(n >>>= 1) != 0
,当长度为16(10000)会触发5次,挨着当前threadlocal的Entry的连续5个都没有引用为null的话,就不会继续往下移除了。所以如果频繁的调用set
方法,它也会帮助清除一些之前key已经被gc掉的entry对象,但无论如何如果没有gc和调用set
方法的话,这些entry对象会一直在内存中占用。
所以每次在使用完threadlocal时要调用一下remove
方法,它会自动把entry移除。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
除此之外在threadlocal时,尽量把它设置为pricate static变量,这样因为threadLocal的强引用一直存在,不会被垃圾回收掉这样就能保证任何时间都可以找到Entry,并对其进行remove
。
Entry的key设置为强引用可以么?
当ThreadLocal的引用在用户栈中已经移除了,并且没有调用remove
方法;但是entry还有一个强引用指向threadLocal对象,e.get()
永远都不会是空,此时entry对象就永远无法被回收掉了。
这样弱引用比强引用就多一层保障弱引用的 ThreadLocal 会被回收,对应value在下一次 ThreadLocaI 调用 get()/set()/remove() 中的任一方法的时候会被清除,从而避免内存泄漏。
示例:
public static void main(String[] args) {
for (int i = 0 ; i < 100 ; i ++){
ThreadLocal temp = new ThreadLocal();
temp.set(i);
temp = null;
}
// System.gc();
ThreadLocal m = new ThreadLocal();
m.set("value");
}
感兴趣的话,可以用上述示例跟着源码跑一遍源码的流程,当开启System.gc();
时可以走到清理回收阶段。
子线程可以使用父线程的threadLocal中的值么?
不可以,如果想使用的话可以采用InheritableThreadLocal
,它会在初始化子线程时进行设置子线程的threadlocal,也仅仅在初始化时有关联,后续子线程和父线程互相更改threadlocal都不会有任何影响。示例:
private static InheritableThreadLocal threadLocal = new InheritableThreadLocal();
@SneakyThrows
public static void main(String[] args) {
threadLocal.set("1");
Thread thread = new Thread(
() -> {
System.out.println("子线程获取threadLocal的值为:" + threadLocal.get());
threadLocal.set("2");
}
);
thread.start();
Thread.sleep(200);
System.out.println("父线程获取threadLocal的值为:" + threadLocal.get());
}
1、父线程先设置threadLocal的值为1;
2、开启一个子线程,获取threadLocal的值,得到结果为1;
3、子线程设置threadLocal为2,并且get一下,得到的结果为2;
4、睡眠200ms确保子线程命令都执行完成;
5、父线程获取threadLocal的值,得到的结果为1。