ThreadLocal:ThreadLocal是线程局部变量,所谓的线程局部变量,就是仅仅只能被本线程访问,不能在线程之间进行共享访问的变量。
ThreadLocal的使用非常广泛,典型的,mybatis的分页插件PageHelper用的就是ThreadLocal。
在我们日常的开发里,最典型的应用就是例如一个请求(单线程)的执行过程要执行很多方法:a->b->c->d->e,假设方法a要用到一个变量,e也要用到这个变量,如果这个变量一直往下传则会显得很臃肿,这个时候,ThreadLocal是个很好的解决方式
ThreadLocal使用方式
直接看一段代码:
public class ThreadLocalTest {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
set();
System.out.println(get()); // 打印 abc
}
private static String get() {
return threadLocal.get();
}
private static void set() {
threadLocal.set("abc");
}
}
代码很简单,功能也很清晰。下面我们来看看ThreadLocal是如何做到在一个地方set而在另一个地方get的
ThreadLocal的简单分析
1.首先我们来看一下ThreadLocal的初始化
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
跟进去看一下底层源码:
/**
* Creates a thread local variable.
* @see #withInitial(java.util.function.Supplier)
*/
public ThreadLocal() {
}
发现啥事也没做,也就说,就简单地实例化了一个对象
再来看看它的成员变量赋值情况
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
这段代码比较迷糊,好像就是为了初始化一个常量——threadLocalHashCode,等会我们再回来看看作用
2.再来看看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);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
从这里看来,ThreadLocal存放的东西的确是跟当前的线程是有关系的,从字面上来理解,字段是存放在了一个map里,而这个map是当前线程的一个成员变量,这个成员变量的类型是ThreadLocalMap。
下面我们来看看这个ThreadLocalMap是什么东西
我们来看看这个方法
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
看看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);
}
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
这里我们第1步的比较迷糊的东西出现了——threadLocalHashCode
看看这段代码做了什么,这段代码的逻辑最终作用是——把该ThreadLocal对应的值存在一个成员变量table里,以key/value的形式存储,key是当前的ThreadLocal实例,value就是我们要保存的值
到这里,我们ThreadLocal存值的原理基本解释完毕了,但是还是有遗留问题
(1)Entry
看看Entry的定义(是ThreadLocalMap一个静态内部类)
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
要非常注意,存储ThreadLocal和它的值的是一个弱引用
关于弱引用的意义可以看我这篇文章 强引用、弱引用、软引用和虚引用
所以,当GC发生的时候,定义的ThreadLocal是有可能被回收的
这里讲一下为什么Entry的key(ThreadLocal)要设置成弱引用
ps:这里我们可以看到,这个弱引用的引用者是ThreadLocal,也就是这个Entry的key
我们知道,弱引用有一个特点:当VM在GC的时候,如果这个对象只有弱引用指向,则该对象不管是否存活,都要被回收
那我们的Entry什么情况下会产生这个情况呢?答案就是我们外部的ThreadLocal的生命周期结束了,因为这样就只有软引用定义的引用者(也就是这个ThreadLocal,在这里是Entry的key)在指向这个对象;如果ThreadLocal的生命周期没结束的话,这个ThreadLocal肯定会有强引用在指向的
如果一旦发生ThreadLocal生命周期结束,但是又没有清空Entry(Entry没被清空,Entry的key也就是ThreadLocal还在使用)这种情况,并且又是强引用,会发生什么情况?就会发生如果这个线程不消亡,这个对象就回收不掉的情况,但是这个对象又是可达的(有Entry的key指向),这就产生了 可达但不使用 的情况,就是我们说的内存泄漏
但是如果我们设置成是弱引用,就能尽可能避免这个问题(线程执行完但是Entry没被清,下一次GC的时候,就能把这个对象回收了)
ps:这里的value是不能被定义成弱引用的,因为外部没有强引用指向它,但是key(ThreadLocal)用强引用,这点用ThreadLocal本身作为key的设计还是挺巧妙的
问题到这里,可能又有人有行的问题了——既然Entry里只有key被设置成弱引用,value没有设置,那岂不是value会很容易产生内存泄漏的问题?(因为这时候key被回收了,也就是key变成了null,但是value还是强引用,对象还在堆里,并且可达不使用,就是在ThreadLocalMap的Entry里产生了一堆key为null的东西)
的确是这样的,但是这个问题,jdk的设计者在设计的时候就在一定程度上进行了缓解——我们在调用ThreadLocal的get/set/remove的时候,底层源码会自动地把这一堆key为null的东西删除了,以便下一次GC把value回收掉
(2)threadLocalHashCode到底用来做什么的
这个参数,我们是用来唯一确定一个ThreadLocal对象的
但是如何保证两个同时实例化的ThreadLocal对象有不同的threadLocalHashCode属性呢?在ThreadLocal类中,还包含了一个static修饰的AtomicInteger成员变量和一个static final修饰的常量(作为两个相邻nextHashCode的差值)。由于nextHashCode是类变量,所以每一次调用ThreadLocal类都可以保证nextHashCode被更新到新的值,并且下一次调用ThreadLocal类这个被更新的值仍然可用,同时AtomicInteger保证了nextHashCode自增的原子性。
确定了唯一的ThreadLocal对象,threadLocalHashCode还作为确定当前线程的ThreadLocalMap的table数组的位置(table数组其实就是Entry数组)
为什么不直接用线程id来作为ThreadLocalMap的key?
这一点很容易理解,因为直接用线程id来作为ThreadLocalMap的key,无法区分放入ThreadLocalMap中的多个value。比如我们放入了两个字符串,你如何知道我要取出来的是哪一个字符串呢?
而使用ThreadLocal作为key就不一样了,由于每一个ThreadLocal对象都可以由threadLocalHashCode属性唯一区分或者说每一个ThreadLocal对象都可以由这个对象的名字唯一区分,所以可以用不同的ThreadLocal作为key,区分不同的value,方便存取。
3.来看看当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)]) {
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();
}
来看看关键代码——e = tab[i = nextIndex(i, len)]
从这里可以看出,如果产生了hash冲突,ThreadLocalMap采用的是再哈希的方式解决冲突的
一张图描绘Thread的存储结构