ThreadLocal源码分析,
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);
/**
* 这里做了一些业务操作<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
,具体流程可以看下面的图:
我们先来回顾一下什么是线程安全的程序呢?在这里体现就是当一个线程setOne()
之后再执行getOne()
看到的结果应该跟前面set
进去的结果一致。我们先来执行一下代码看看结果:
从执行结果我们可以看到getOne
()的结果跟我们前面setOne()
值是一样都是0,但是对于setThree()
和getThree()
确不一样明显出现了线程安全的问题。我们画图分析:
[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 void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//从线程中获取threadLocals
ThreadLocalMap map = getMap(t);
if (map != null)
//从这里可以看到ThreadLocalMap中key为threadLocal这个对象
map.set(this, value);
else
//如果该线程对应的threadLocalMap还没有实例化,实例化一个保证每个线程都自己的threadLocalMap
createMap(t, value);
}
经过分析,我们发现当调用ThreadLocal.set
方法的时候,会调用getMap
会直接返回调用线程对象里的threadLocals
,这样保证了每个线程有自己单独的threadLocalMap对象互相不影响。
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
的源码如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
从这里代码也可以看出Entry
的key为ThreadLocal
,value
为你要设置的值,我们首先可以看到Entry继承了WeakReference
,WeakReference是弱引用的意思,至于什么是弱引用等会再说,在构造方法Entry
里先调用super(k)
调用了父类的方法WeakReference(T referent)
,那么我们认为Entry
中只有key是弱引用,即threadLocalMap中的key是弱引用,从上图中Entry有个虚线的箭头就表示这个是弱引用的意思。
那么现在来了,为什么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[]
这个对象再执行垃圾回收之后会被清除掉。
ThreadLocal的内存泄漏问题
现在大部分框架都使用线程池来使用线程,使用线程池意味着线程用完会放回池子里不会被销毁。那么可能出现下面的情况:
有可能我们ThreadLocal RESOURCE_1
是一个局部变量或者RESOURCE_1被置为nullRESOURCE_1 = null
,那么threadLocalMap中的value永远访问不到,我们是通过String value = ResourceUtil.RESOURCE_1.get();
这样的代码拿到value的,如果RESOURCE_1=null
了,那么value永远就拿不到。但是从图中我们也可以看到有这样一个链条thread_1->threadLocalMap->entry->value
,那么意味着这value
永远不会回收。不再用的对象回收不了这就造成了内存泄漏的问题,如果有太多对象回收不了可能就会造成OOM
的情况。
这个时候我们看看弱引用怎么派上用场的,如果发生了垃圾回收的情况(不管是MinGC还是FullGC)FIXME MinGC的频率
,
由于是弱引用,当RESOURCE_1=null
后,ThreadLocal对象
只被Entry中的key引用,但是我们发现Entry中的key是通过弱引用指向ThreadLocal对象
的(实际上key并不是ThreadLocal本身,而是它的一个弱引用)
,根据弱引用的特性,如果进行了垃圾回收那么弱引用的对象也会被回收掉,即ThreadLocal
对象会被回收掉,这个时候Entry中的key就变成了null。这样时候我们往前进了一步,但是发现value还是在,还是有内存泄漏。
为了降低内存泄漏的几率,JDK大牛们设计推荐我们再使用了ThreadLocal用完后手动执行 remove()
方法。为什么呢?我们看下ThreadLocal.remove()
的代码。
//ThreadLocal.java
//先获取这个线程对应的threadLocalMap
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
我们可以看到,执行remove方法时候先得到这个线程对应的threadLocalMap
,比如thread_1
就拿到了上面红色对应的map。
我们点进入看remove方法,可以看到通过这个key找到对应的Entry
,然后执行Entry.clear()
,接着还执行了expungeStaleEntry(i)
,这个方法是把key为null的Entry
清除。
//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;
}
}
}
我们再仔细阅读ThreadLocal.set()/get()
方法发现,再这几个方法中也都调用了expungeStaleEntry()
方法来清除Entry
中key为null的value。那么也有人会问,我自己每次用完就手动remove()
不就可以了为什么使用弱引用,但是实际在写业务代码的时候,很多人可能意识不到这么深的问题或者粗知大意忘记了调用remove()
方法。
如果用强引用,忘记调用remove()
方法肯定会产生内存泄漏。如下图所示:
如果是强引用的话,那么threadLocalMap
中Entry的key一直存在 ,没有办法判断哪些key还在被使用哪些没有使用。
但是如果使用弱引用,在垃圾回收之后就会产生key=null的Entry,这样的Entry在我们下次调用set()/get()
的时候会通过 expungeStaleEntry(i)
清除,给我们增加了一层保障。
相关文章
- 暂无相关文章
用户点评