ThreadLocal原理剖析
JAVA四种引用类型(强软弱虚)
分析ThreadLocal之前先了解java下面的四种引用类型:
强引用
形式:Person person = new Person();
特点:如果没有把person置为空的话,在继续在堆里面加东西的时候,就算heap装不下了,宁愿出现OOM,也不回收person
软引用
形式:SoftReference<byte[]> SR = new SoftReference<>(new byte[1024]);
特点:heap如果装不下的时候,gc会回收软引用
应用场景:经常用作缓存
弱引用
形式:WeakReference<Person> Wr = new WeakReference<>(new Person());
特点:当gc看到弱引用的时候,gc会回收它。
引用场景:解决ThreadLocal的内存泄露问题
虚引用(了解即可)
形式:PhantomReference<Person> PR = new PhantomReference<Person>(new Person(),new ReferenceQueue());
特点:管理直接内存,起到通知作用,当gc回收虚引用的时候,会把引用的对象放到一个队列里面。
ThreadLocal原理解析(深入源码)
我们看看以下的代码:
public class ThreadLocalTest {
static ThreadLocal<Integer> threadLocal = new ThreadLocal();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadLocal.get());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadLocal.set(10);
}
}).start();
}
}
运行之后控制台显示:
我们可以看到我们在第二个线程把10作为ThreadLocal的set方法的参数传进去,但是第二个线程是读不到10的,这就说明了,ThreadLocal是起到了隔离线程的一个作用。接下来,我们进入ThreadLocal的set方法看看发生了什么:
public void set(T value) {
//取得当前线程
Thread t = Thread.currentThread();
//把当前线程作为参数传入getMap获得一个Map对象
ThreadLocalMap map = getMap(t);
//把ThreadLocal对象作为key传入到上面那个map中
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
接下来我们看看getMap方法是如何获取ThreadLocalMap对象的:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
我们惊奇的发现getMap方法返回的是线程的一个属性,我们进入Thread的源码看看这个属性:
ThreadLocal.ThreadLocalMap threadLocals = null;
我们发现ThreadLocalMap 是ThreadLocal的一个静态内部类,经过如上分析我们不难发现,ThreadLocal可以实现线程隔离的原因了,我们很容易可以知道每个线程都有一个ThreadLocalMap ,当我们在当前线程调用ThreadLocal的get方法的时候,是从他的ThreadLocalMap里面找到,所以不会和其他线程出现混乱。接下我们来看看ThreadLocalMap是如何存放数据的,我们看到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;
}
}
我们惊奇的发现ThreadLocalMap 里面存放的键值对是一个弱引用,并且他的key是ThreadLocal的对象,并且把它作为参数传入父类的构造器,就是说当没有强引用指向key的时候,他会被gc垃圾回收掉。然后ThreadLocalMap是以数组的方式存储键值对的:
private Entry[] table;
我们接着回到ThreadLocal的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);
}
我们可以看到map.set(this, value);
,所以我们可以知道threadLocal.set(10);
这个方法就是将threadLocal实例作为key,10作为value存入到当前线程的ThreadLocalMap中,经过以上分析我们可以画出以下的图片(实线表示强引用,虚线表示软引用):
ThreadLocal的内存泄露问题
为什么要使用弱引用?
我们观察上面的图,如果Entry是强引用,当我们在当前线程中不使用ThreadLocal的时候,我们把它设为了null,但是因为Entry是的key强引用的指向上面不需要再使用的ThreadLocal对象,他会一直存放在内存中,从而造成了内存泄漏,但是如果Entry键值对是弱引用的话,当ThreadLocal不使用的时候,就没东西指向它了,它自然就被gc回收避免了内存泄漏。
可能造成内存泄漏的第二种原因
我们接着上面的问题,当key被回收了之后,他是一个null,但是我们的value的值还是强引用的指向了一个对象,因为key为null我们也不可能使用到这个value了,但是它又不能被gc回收所以也造成了内存泄漏。我们的解决方式就是在把ThreadLocal设为空的时候,一定要调用他的remove方法,把这个键值对移除,尽管我们调用的set方法的时候,会把ThreadLocalMap的key为null的键值对移除,因为我们开发的时候一个线程是长时间运行的,同时set方法我们也不知道啥时候调用,所以作为一种编程习惯,在不用ThreadLocal的时候要remove掉。
ThreadLocal的还可能出现的问题
在开发中,我们经常会使用线程池技术,如果我们在线程结束的时候不把当前线程的ThreadLocalMap清空掉,会出现很多问题,因为我们线程结束的时候会把线程放入线程池中,当我们再次从线程池中取线程的时候,当取到一样的线程,我们使用的就是上次的线程的ThreadLocalMap。