ThreadLocal
此类提供线程局部变量。这些变量不同于它们的正常对应变量,因为每个访问一个(通过它的 get
或set
方法)的线程都有它自己的、独立初始化的变量副本。 ThreadLocal
实例通常是希望将状态与线程的类中的私有静态字段(例如,用户 ID)相关联。
场景:一般在连接池优化上会使用到ThreadLocal,避免使用同步,提高性能。
用法
public class Test {
public static ThreadLocal<Integer> local = new ThreadLocal<>();
public static void main(String[] args) {
for(int i=0; i<2;i++) {
new Thread(new Runnable() {
@Override
public void run() {
Double d = Math.random() * 10;
local.set(d.intValue());
new A().get();
new B().get();
}
}).start();
}
}
static class A{
public void get(){
System.out.println(local.get());
}
}
static class B{
public void get(){
System.out.println(local.get());
}
}
}
set
set是用来设置想要在线程本地的数据,可以看到先拿到当前线程,然后获取当前线程的ThreadLocalMap,如果map不存在先创建map,然后设置本地变量值。
public void set(T value) {
Thread t = Thread.currentThread();//先拿到当前线程
ThreadLocalMap map = getMap(t);//获取当前线程的ThreadLocalMap
if (map != null)
map.set(this, value);//设置本地变量值
else
createMap(t, value);//不存在先创建map
}
那ThreadLocalMap又是什么?其实是线程自身的一个成员属性
跟想象中的Map有点不一样,它其实内部是有个Entry数组,将数据包装成静态内部类Entry对象,存储在这个table数组中。
Entry继承自WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
每个线程自身都维护着一个ThreadLocalMap,用来存储线程本地的数据,可以简单理解成ThreadLocalMap的key是ThreadLocal变量,value是线程本地的数据。就这样很简单的实现了线程本地数据存储和交互访问。
get
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);//获取当前线程的ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);//获取Entry
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;//获取保存的值
return result;
}
}
return setInitialValue();
}
remove
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
/**
* Remove the entry for key.
*/
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;
}
}
}
我看看ThreadLocalMap的expungeStaleEntry这个方法,这个方法在ThreadLocalMap get、set、remove、rehash等方法都会调用到。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 将entry赋空
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) { //找到已经被GC的ThreadLocal
e.value = null;//清理掉key为空的值
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
如果数据初始化好之后,一直不调用get、set等方法,这样Entry就一直不能回收,导致内存泄漏。所以一旦数据不使用最好主动remove。
如果线程资源回收了,还会泄漏吗?
当然不会,线程回收后,存在线程相关的ThreadLocalMap也会被回收。大部分场景中,线程都是一直存活或者长时间存活。
问题
内存泄露问题
实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。
如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。
ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。
脏数据问题
由于项目基本上都是使用线程池,因为Thread对象是复用的,那么与Thread对象绑定的ThreadLocal变量也可能被复用。
父子线程共享线程变量
很多场景下通过ThreadLocal来透传全局上下文,会发现子线程的value和主线程不一致。