Java学习2 (ThreadLocal)
ThreadLocal
每个线程的变量副本是存储在哪里的
- ThreadLocal(线程局部变量)
- 在线程之间共享变量是存在风险的,有时可能要避免共享变量,使用ThreadLocal辅助类为各个线程提供各自的实例。
- 如果加锁,那么会开销很大
实现原理:
- ThreadLocal的get方法就是从当前线程的ThreadLocalMap中取出当前线程对应的变量的副本。该Map的key是ThreadLocal对象,value是当前线程对应的变量。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
- 变量是保存在线程中的,而不是保存在ThreadLocal变量中
- 每个线程都有一个这样的名为threadLocals 的ThreadLocalMap,以ThreadLocal和ThreadLocal对象声明的变量类型作为key和value。
- 这样,我们所使用的ThreadLocal变量的实际数据,通过get方法取值的时候,就是通过取出Thread中threadLocals引用的map,然后从这个map中根据当前threadLocal作为参数,取出数据。现在,变量的副本从哪里取出来的(本文章提出的第一个问题)已经确认解决了。
- 每个线程内部都会维护一个类似 HashMap 的对象,称为 ThreadLocalMap,里边会包含若干了 Entry(K-V 键值对),相应的线程被称为这些 Entry 的属主线程;
- Entry 的 Key 是一个 ThreadLocal 实例,Value 是一个线程特有对象。Entry 的作用即是:为其属主线程建立起一个 ThreadLocal 实例与一个线程特有对象之间的对应关系;
- Entry 对 Key 的引用是弱引用;Entry 对 Value 的引用是强引用。
为什么ThreadLocalMap的Key是弱引用
- 如果是强引用,ThreadLocal将无法被释放内存。
- 因为如果这里使用普通的key-value形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。弱引用是Java中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。
ThreadLocalMap是何时初始化的(setInitialValue)
在get时最后一行调用了setInitialValue,它又调用了我们自己重写的initialValue方法获得要线程局部变量对象。ThreadLocalMap没有被初始化的话,便初始化,并设置firstKey和firstValue;如果已经被初始化,那么将key和value放入map。
ThreadLocalMap 原理
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
- 它也是一个类似HashMap的数据结构,但是并没实现Map接口
- 也是初始化一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对,只不过这里的key永远都是ThreadLocal对象,通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中。
- ThreadLoalMap的Entry是继承WeakReference,和HashMap很大的区别是,Entry中没有next字段,所以就不存在链表的情况了。所以如果存在hash冲突则使用的是开放寻址法。
- 在ThreadLocalMap中,形如key.threadLocalHashCode & (table.length - 1)(其中key为一个ThreadLocal实例)这样的代码片段实质上就是在求一个ThreadLocal实例的哈希值,只是在源码实现中没有将其抽为一个公用函数。
对于& (INITIAL_CAPACITY - 1),相对于2的幂作为模数取模,可以用&(2n-1)来替代%2n,位运算比取模效率高很多。至于为什么,因为对2^n取模,只要不是低n位对结果的贡献显然都是0,会影响结果的只能是低n位。
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
内存泄露
- 只有调用TheadLocal的remove或者get、set时才会采取措施去清理被回收的ThreadLocal对应的value(但也未必会清理所有的需要被回收的value)。假如一个局部的ThreadLocal不再需要,如果没有去调用remove方法清除,那么有可能会发生内存泄露。
- 既然已经发现有内存泄露的隐患,自然有应对的策略,在调用ThreadLocal的get()、set()可能会清除ThreadLocalMap中key为null的Entry对象,这样对应的value就没有GC Roots可达了,下次GC的时候就可以被回收,当然如果调用remove方法,肯定会删除对应的Entry对象。
- 如果使用ThreadLocal的set方法之后,没有显式的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法
- JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。