【Java】- ThreadLocal源码分析
背景
在高并发环境下保证线程安全方式除了加锁还有另一种思路就是给每个线程设置单独的值,而这个思路在JDK里对应的实现是ThreadLocal。ThreadLocal可能会产生内存泄漏问题,但是很多人对它只有一个模棱两可的理解?这篇文章通过分析源码的方式来具体解决这个疑问。
ThreadLocal使用场景
我们先通过一个例子演示通过ThreadLocal来解决线程安全的问题,请看下面的代码:
public class ThreadLocalTest {
static class ResourceUtil {
public final static ThreadLocal<String> RESOURCE_1 =
new ThreadLocal<String>();
}
static class A {
private String three;
public void setOne(String value) {
System.out.println("[" + Thread.currentThread().getName() + "]" + " setOne: " + value);
ResourceUtil.RESOURCE_1.set(value);
}
public String getOne()
{
String value = ResourceUtil.RESOURCE_1.get();
System.out.println("[" + Thread.currentThread().getName() + "]" + " getOne: " + value);
return value;
}
public void setThree(String value)
{
three = value;
System.out.println("[" + Thread.currentThread().getName() + "]" + " setThree: " + value);
}
public String getThree()
{
String value = this.three;
System.out.println("[" + Thread.currentThread().getName() + "]" + " getThree: " + value);
return value;
}
}
public static void main(String []args) {
final A a = new A();
for(int i = 0 ; i < 6 ; i ++) {
final String val = " value = (" + i + ")";
new Thread("threadName-" + i) {
public void run() {
try {
a.setOne(val);
a.setThree(val);
/**
* Thread.sleep(5)模拟这里做了一些业务操作<br>
*/
Thread.sleep(5);
a.getOne();
a.getThree();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
ResourceUtil.RESOURCE_1.remove();
}
}
}.start();
}
}
}
这段代码的main启动了6个线程,每个线程都分别执行了setOne(),getThree(),setThree(),getThree,具体流程可以看下面的图:

我们先来回顾一下什么是线程安全的程序呢?在这里体现就是当一个线程setXXX()之后再执行getXXX()看到的结果应该跟前面set进去的值一致。我们先来执行一下代码看看结果:

从执行结果我们可以threadName-0线程执行时getOne()的结果跟我们前面setOne()值是一样都是0,但是对于setThree()和getThree()确不一样(threadName-0设置为0但是获取的是5),这就是出现了线程安全的问题。我们画图分析:
[threadName-0] getThree: value=(5)结果不是0这个容易分析,是由于A对象的A.three这个变量被其他线程改了。而setOne()和getOne变量执行结果却没有线程安全问题,我们猜测每个线程有个副本(见上图),即Thread_0对应存的是value=(0),Thread_5对应存的是value=(5), 但是具体怎么存的呢,我们需要阅读对应的源码。
ThreadLocal怎么解决线程安全问题:存储线程级别数据
上面的代码,当我们执行RESOURCE_1.set()的时候,执行的是ThreadLocal.set()方法,我们分析这个方法:
public class ThreadLocal{
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//从线程中获取threadLocals(为threadLocalMap对象)
ThreadLocalMap map = getMap(t);
if (map != null)
//从这里可以看到ThreadLocalMap中key为threadLocal这个对象
map.set(this, value);
else
//如果该线程对应的threadLocalMap还没有实例化,实例化一个保证每个线程都自己的threadLocalMap
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
//从t.threadLocals看出,这个threadLocals是线程t对象里的成员,所以每个线程都有自己的threadLocalMap
return t.threadLocals;
}
}
a). 经过分析,我们发现当调用ThreadLocal.set方法的时候,getMap会直接返回调用线程对象里的threadLocals,这样保证了每个线程有自己单独的threadLocalMap对象互相不影响。
b). 如果不存在,则创建一个ThreadLocalMap。

好,分析到这里,我们知道通过threadLocal,每个线程都存储自己独立的数据,具体在thread.threadLocals里面。既然每个线程都有自己的副本,那么就不会有共享变量不会有线程安全的问题。下面我们分析threadLocal使用时内存泄漏问题。
ThreadLocal内存泄漏问题
我们平常都是这样使用ThreadLocal的,比如上面代码ResourceUtil.RESOURCE_1.set(value);那么这段代码在栈和堆里会分别生成哪些数据呢?

具体生成的数据如上图,再栈上会生成变量RESOURCE_1,Thread_1,Thread_2变量,堆中有Thread_1对象,Thread_2对象,这个线程对象中里面有threadLocalMap成员变量,而这个threadLocalMap中的key为threadLocal本身,这个我们从上面代码ThreadLocal.set()方法中的map.set(this,value)可以看出。
threadLocalMap底层是通过Entry存储的,我们看到ThreadLocalMap中的Entry的源码如下:
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
//Entry 传进来的key为ThreadLocal,super(k)为经过了reference的包装
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
}
从这里代码也可以看出Entry的key为ThreadLocal,value为你要设置的值,当我们想往threadLocalMap中set会最终会在Enty[]数组里增加一个条目或者修改已有条目的值,这个可以具体查看threadLocalMap.set(ThreadLocal<?> key, Object value) 方法,跟hashMap操作类似。
我们重点看到Entry继承了WeakReference,WeakReference是弱引用的意思,至于什么是弱引用等会再说,在构造方法Entry里先调用super(k)调用了父类的方法WeakReference(T referent),那么我们认为Entry中只有key是弱引用。既可以认为threadLocal会被WeakReference弱引用包装,具体可以查看下面的调试结果图:

从上图也可以看出Entry中key是WeakReference,而WeakReference中的referent为threadLocal。
那么现在来了,为什么Entry的key为什么使用弱引用呢?弱引用有什么特点呢?这个跟内存泄漏又有什么关系呢?下面我们先分析一下弱引用的特性。
弱引用的特性
弱引用什么特点呢?我们先看一段代码:
public static void main(String[] args) {
WeakReference<byte[]> wrObj = new WeakReference<byte[]>(new byte[1024*1024*10]);
System.out.println("before gc " + wrObj.get());
System.gc();
System.out.println("after gc "+ wrObj.get());
}
这段代码的执行结果如下:
before gc [B@1b6d3586
after gc null
分析结果我们发现,弱引用中的数据在执行gc后被回收了,这个就是弱引用的特点。为了更好的理解弱引用,我们先说一说各种引用的特点:
如果 写了这样一段代码:Object o = new Object(),这是强引用在内存中是这样的:

对于上面的代码WeakReference<byte[]> wrObj = new WeakReference<byte[]>(new byte[1024*1024*10]);其内存中是这样的:

从图中可以看出wrObj指向 WeakReference是强引用,但是WeakReference中包装的数据是弱引用指向的,这样new Byte[]这个对象再执行垃圾回收之后会被清除掉,但是WeakReference对象本身不会被回收。
对于本文由于Entry<key>继承了WeakReference,同样对应的内存布局如下图:

当ThreadLocal类型对象不在使用,即上面例子中的RESOURCE_1不再被使用(手动置为null或者作用域消失),如果这个时候执行垃圾回收,那么threadLocal对象就会被回收调用,因为WeakReference持有ThreaLocal对象的引用但是这个是弱引用。垃圾回收后结果如下图所示:

ThreadLocal的内存泄漏问题
内存泄漏问题怎么发生
现在大部分框架都使用线程池来使用线程,使用线程池意味着线程用完会放回池子里不会被销毁。那么可能出现下面的情况:

有可能我们ThreadLocal RESOURCE_1是一个局部变量或者RESOURCE_1被置为null,那么threadLocalMap中的value永远访问不到,我们是通过String value = ResourceUtil.RESOURCE_1.get();这样的代码拿到value的,如果RESOURCE_1=null了,那么value永远就拿不到,因为你无法调用RESOURCE_1.get()了。但是从图中我们也可以看到有这样一个链条thread_1->threadLocalMap->entry->value,那么意味着这value永远不会被回收。不再用的对象回收不了这就造成了内存泄漏的问题,如果有太多对象回收不了可能就会造成OOM的情况。就像上面图中value = (1)和value = (2) 访问不到造成内存泄露了。
使用弱引用标记不再用的Entry

这个时候我们看看弱引用怎么派上用场的,如果发生了垃圾回收的情况(不管是MinGC还是FullGC),,
由于是弱引用,当RESOURCE_1=null后,ThreadLocal对象只被Entry中的key引用,但是我们发现Entry中的key是通过弱引用指向ThreadLocal对象的(实际上key并不是ThreadLocal本身,而是它的一个弱引用),根据弱引用的特性,如果进行了垃圾回收那么弱引用的对象也会被回收掉,即ThreadLocal对象会被回收掉,这个时候Entry中的key的reference就变成了null。这样时候我们往前进了一步,我们可以通过key.reference=null知道哪些Entry不再会被使用。当然这个时候value还是在,还是有内存泄漏问题,但是我们可以手动的把key.reference=null对应的entry清除掉。这就是为什么很多网上的文章推荐我们再使用了ThreadLocal用完后手动执行 remove()方法。为什么呢?我们看下ThreadLocal.remove()的代码干了什么事情。
调用remove方法删除不再用的Entry
我们可以看到,执行remove方法时候先得到这个线程对应的threadLocalMap,比如thread_1就拿到了上面红色对应的map。

我们点进入看remove方法,可以看到通过这个key找到对应的Entry,然后执行Entry.clear(),接着还执行了expungeStaleEntry(i),这个方法会做两件事情把remove()参数传过来的key对应的entry删除掉。
我们平常写代码最好也是养成这样一个习惯,不再使用了的应该删除掉(业务上已经不再需要):
new Thread("threadName-" + i) {
public void run() {
try {
a.setOne(val);
//a.setThree(val);
/**
* 这里做了一些业务操作<br>
*/
Thread.sleep(5);
a.getOne();
//a.getThree();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
ResourceUtil.RESOURCE_1.remove(); //把threadLocal中对应的key删除掉
}
}
}.start();
但实际情况中,我们工作中有时会忘记调用remove()方法,那么threadLoalMap中存在大量key.reference=null对应的entry造成内存泄漏问题。仔细阅读remove()方法发现在该方法后面调用了 expungeStaleEntry(i);方法,该方法就是把key.reference=null对应的entry删除掉。
当执行了 expungeStaleEntry(i);方法后,堆中内存会如下图所示:

更多保障:处理不再用Entry
我们再仔细阅读ThreadLocal.set()/get()方法发现,再这几个方法中也都调用了expungeStaleEntry()方法来清除Entry中key为null的value。
查看JDK的代码,具体调用链如下:
get()-> getEntryAfterMiss() -> expungeStaleEntry
set()-> replaceStaleEntry()
你看JDK设计者考虑问题就很严谨,你每次set()之后有可能忘记调用remove方法,所以JDK设计者在执行get()和set()方法的时候也会清理一遍,最大程度的减少内存泄漏的发生。
问题答疑:
-
为什么要用强引用:
答:如果是强引用的话,那么
threadLocalMap中Entry的key一直存在 ,没有办法判断哪些key还在被使用哪些没有使用。 -
使用弱引用可以杜绝内存泄漏吗?
答:不能,但是如果使用弱引用,在垃圾回收之后就会产生key=null的Entry,这样的Entry在我们下次调用
set()/get()的时候会通过expungeStaleEntry(i)清除,给我们增加了一层保障。
如果想在一个线程中保留用户的名字和用户的id怎么办?用一个threaLocal的话?
-->
附:相关概念
内存泄漏
内存泄漏是指系统中分配的内存不再使用但未释放造成内存的空间浪费。
附:代码参考
Entry.get
class ThreadLocal{
static class ThreadLocalMap{
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//参数传进来的是threadLocal,e.get()调用的是Re
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
}
}
public abstract class Reference{
public T get() {
return this.referent;
}
}
threadLocal.remove
//ThreadLocal.java
//先获取这个线程对应的threadLocalMap
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
//threadLocal
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) {
//清楚key和value
e.clear();
expungeStaleEntry(i);
return;
}
}
}
6.4 threadLocal.expungeStaleEntry
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; //把value变成null
tab[i] = null; //把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;
}

浙公网安备 33010602011771号