一.理清ThreadLocal与Thread和ThreadLocalMap的关系
首先,在Thread类中有一行,说明:ThreadLocalMap是Thread类的一个成员变量。
在ThreadLocal类中又有这样一段代码,说明ThreadLocalMap是ThreadLocal类的内部类。
二.ThreadLocal的原理:
1.ThreadLocal是什么?
首先,它是一个数据结构,有点像HashMap,可以保存"key : value"键值对,但是一个ThreadLocal只能保存一个,并且各个线程的数据互不干扰。
ThreadLocal<String> localName = new ThreadLocal<>();
localName.set("测试");
String name = localName.get();
在线程1中初始化了一个ThreadLocal对象localName,并通过set方法,保存了一个值 “测试”,同时在线程1中通过 localName.get()可以拿到之前设置的值,但是如果在线程2中,拿到的将是一个null。
这是为什么,如何实现?不过之前也说了,ThreadLocal保证了各个线程的数据互不干扰。
看看 set(T value)和 get()方法的源码
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
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();
}
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可以发现,每个线程中都有一个 ThreadLocalMap数据结构,当执行set方法时,其值是保存在当前线程的 threadLocals变量中,当执行get方法时,是从当前线程的 threadLocals变量获取。
所以在线程1中set的值,对线程2来说是摸不到的,而且在线程2中重新set的话,也不会影响到线程1中的值,保证了线程之间不会相互干扰。
那每个线程中的 ThreadLoalMap究竟是什么?
2.ThreadLoalMap
它也是一个类似HashMap的数据结构,但它是在ThreadLocal中的内部类,并没实现Map接口。
我们可以看出每个Thread维护一个ThreadLocalMap,存储在ThreadLocalMap内的就是一个以Entry为元素的table数组,Entry就是一个key-value结构,key为ThreadLocal,value为存储的值。类比HashMap的实现,其实就是每个线程借助于一个哈希表,存储线程独立的值。我们可以看看Entry的定义:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
... ...
}
在ThreadLoalMap中,也是初始化一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对,只不过这里的key永远都是ThreadLocal对象,是不是很神奇,通过ThreadLocal对象的set方法,把ThreadLocal对象自己当做key,放进了ThreadLoalMap中
ThreadLoalMap的Entry是继承WeakReference,和HashMap很大的区别是,Entry中没有next字段,所以就不存在链表的情况了。
3.hash冲突
没有链表结构,那发生hash冲突了怎么办?
先看看ThreadLoalMap中插入一个key-value的实现
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
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();
}
每个ThreadLocal对象都有一个hash值 threadLocalHashCode,每初始化一个ThreadLocal对象,hash值就增加一个固定的大小 0x61c88647。
在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,
过程如下:
1、如果当前位置是空的,那么正好,就初始化一个Entry对象放在位置i上;
2、不巧,位置i已经有Entry对象了,如果这个Entry对象的key正好是即将设置的key,那么重新设置Entry中的value;
3、很不巧,位置i的Entry对象,和即将设置的key没关系,那么只能找下一个空位置;
这样的话,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置
可以发现,set和get如果冲突严重的话,效率很低,因为ThreadLoalMap是Thread的一个属性,所以即使在自己的代码中控制了设置的元素个数,但还是不能控制其它代码的行为。
4.内存泄露
这里ThreadLocal和key之间的线是虚线,因为Entry是继承了WeakReference实现的,当ThreadLocal Ref销毁时,指向堆中ThreadLocal实例的唯一一条强引用消失了,只有Entry有一条指向ThreadLocal实例的弱引用,假设你知道弱引用的特性,那么这里ThreadLocal实例是可以被GC掉的。这时Entry里的key为null了,那么直到线程结束前,Entry中的value都是无法回收的,这里可能产生内存泄露,后面会说如何解决。
(1)为什么这里的ThreadLocalMap要使用table数组存储,为什么不采用MAP?
因为对于一个线程中,可能存在多个ThreadLocal实例或者使用ThreadLocal的类多次实例化。
(2)那么这里又为什么不使用Map呢? 在ThreadLocalMap中为什么采用弱引用Entry,保存ThreadLocal实例与其对应的值?
假设使用MAP数据结构,
因为ThreadLocal的生命周期与使用ThreadLocal的类同步,当ThreadLocal的使用类的生命周期结束了,但是Thread可能还存在, Map中仍然在引用ThreadLocal实例,ThreadLocal实例一直得不到释放,造成内存泄漏。
那么采用数组也存在同样的问题,如何解决呢? 这里定义了Entry类,继承WeakReference实现的。
当ThreadLocal Ref销毁时,指向堆中ThreadLocal实例的唯一一条强引用消失了,只有Entry有一条指向ThreadLocal实例的弱引用,根据弱引用的特性,那么这里ThreadLocal实例是可以被GC掉的。
这时Entry里的key为null了,那么直到线程结束前,Entry中的value都是无法回收的,这里也可能产生内存泄露。
总结来说:主要是ThreadLocal与使用ThreadLocal的类的生命周期绑定, 与Thread的生命周期不同步,导致可能存在内存泄漏的问题。
如何避免内存泄露?
简单来说就是,当每次在对ThreadLocalMap做get,set操作时都会不断清理掉这种key为null的Entry的,判断table中是否有key为null的entry,有就把他清除掉。当然如果调用remove方法,肯定会删除对应的Entry对象。
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
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();
}
}
如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要:
1 .使用者需要手动调用remove函数,删除不再使用的ThreadLocal.
2 .还有尽量将ThreadLocal设置成private static
的,这样ThreadLocal会尽量和线程本身一起消亡。
三.强软弱虚引用
在JVM的垃圾回收中,需要判断对象的存活状态。而无论是通过引用计数法判断对象的引用数量,还是通过可达性分析法判断对象的引用链是否可达,判断对象存活与否都与引用有关。在JDK1.2之前,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但是太过狭隘。在这种定义下,一个对象只有被引用或者没有被引用两种状态,对于如何描述一些“食之无味,弃之可惜”的对象就无能为力了。
在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种。这4种引用的强度依次减弱:
1.强引用就是指在程序中普遍存在的,类似“Object obj = new Object()”这类的引用。只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
2.软引用是用来描述一些还有用但非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列入回收范围内进行二次回收。如果这次回收还没有足够的内存,才会发生内存溢出异常。
3.弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能存活到下次GC之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被若引用关联的对象。
4.虚引用也称为幽灵引用或幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用存在,完全不会对其生存时间构成影响,也无法通过一个虚引用来获取一个对象的实例。为对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。