一、前置知识
1.1 ThreadLocal的作用
ThreadLocal主要起到线程隔离的作用,每个线程可以通过ThreadLocal存放自己私有的数据。线程1存放的数据,只有线程1能拿到,其它线程都不能获取到该数据。应用场景主要有:
- Spring框架中的事务功能,@transactional注解的处理类将数据库连接对象放在了ThreadLocal中
- Mybatis框架中分页功能也使用了ThreadLocal
1.2 Java中的四种引用类型
- 强引用:JVM宁可报OOM,也不会回收强引用所指向的对象。
- 软引用:当堆空间不足时,JVM就会回收软引用所指向的对象。应用场景:缓存。
- 弱引用:只要进行垃圾回收,就会回收弱引用所指向的对象。应用场景:ThreadLocal防止内存泄漏。
- 虚引用:只有一个作用——管理直接内存。直接内存是指操作系统所管理的内存,一个Java程序一般都是运行在JVM所管理的内存中,而JVM中会有一个指向直接内存地址的引用。当虚引用指向的对象回收时会分为两步:(1)先回收JVM中指向直接内存地址的引用,并将这个引用放置到一个队列中。(2)将队列中引用指向的直接内存上的数据回收掉。我们可以监控存放指向直接内存地址的队列,从而知道虚引用指向的对象什么时候被回收了,也就是起到了一个通知作用。
二、ThreadLocal源码分析
2.1 ThreadLocal的使用
public class ThreadLocalDemo {
private static final ThreadLocal<Person> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
//线程1在threadLocal中存数据
new Thread(() -> {
threadLocal.set(new Person());
}).start();
//线程2从threadLocal中取数据
new Thread(() -> {
System.out.println(threadLocal.get());
}).start();
}
static class Person{
String name = "zhangsan";
}
}
执行结果:
从结果中可以看到,ThreadLocal是具有线程隔离效果的。线程1在ThreadLocal中存入Person对象,线程2是无法取出这个Person对象的。
2.2 ThreadLocal源码结构
我们主要关注红框标注的几个最重要的方法与内部类。
- ThreadLocalMap静态内部类
- get()
- set(T)
- remove()
- getMap(Thread)
- createMap(Thread, T)
2.2.1 get()方法
public T get() {
//获取当前线程对象
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
/**
* 从当前线程的ThreadLocalMap对象中获取key=this的value,这里this表示的是
* ThreadLocal对象,而key所对应的value就是我们存放在ThreadLocal的值
**/
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
从get()
方法我们可以知道,我们往ThreadLocal对象存值时,值其实是被放在了当前线程所对应的ThreadLocalMap中,那么这个ThreadLocalMap又是何物?
2.2.2 ThreadLocalMap静态内部类
既然是一个类,那么我们首先看一下这个类的结构是怎样的。
首先我们看一下它的构造函数,看看它底层到底是存储的是什么数据
//private static final int INITIAL_CAPACITY = 16;
//private Entry[] table;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//ThreadLocalMap底层原来是一个Entry数组,每一个Entry对象表示的就是一对key,value数据
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
从这个构造函数,我们可以初步画出ThreadLocalMap底层的存储结构。ThreadLocalMap底层是一个数组,数组中每个元素存放的是指向Entry对象的引用。
在ThreadLocalMap的源码结构图中,我们可以看到Entry类是ThreadLocalMap的静态内部类,我们来分析一下它的源码:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
//前面分析过,Entry中的key其实就是ThreadLocal对象,这里super(k)表示,指向ThreadLocal对象的引用是一个弱引用
super(k);
value = v;
}
}
既然我们知道了Entry类中的key是一个弱引用,那么ThreadLocalMap的底层图,我们可以画的更具体了。
到这里我们把ThreadLocalMap的结构就分析清楚了,那么我们还未解释为什么ThreadLocal可以实现线程隔离,不要急,等我分析完接下来的set()
方法,然后再解释。
2.2.3 ThreadLocal的set()方法
public void set(T value) {
//获取当前线程对象
Thread t = Thread.currentThread();
//通过当前线程获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
//若map存在,则将key=ThreadLocal,value=value的键值对存进map
map.set(this, value);
else
//若map不存在,则创建map,并将key,value存进map
createMap(t, value);
}
2.2.4 ThreadLocal的getMap()方法
ThreadLocalMap getMap(Thread t) {
//返回当前线程对象的threadLocals属性,这是实现线程隔离的关键
return t.threadLocals;
}
我们发现threadLocals
是Thread类中的一个属性,那么也就是说,每个线程都有一个threadLocals,这个属性是每个线程所私有的。
2.2.5 ThreadLocal的createMap()方法
void createMap(Thread t, T firstValue) {
//给每个线程都创建了一个ThreadLocalMap对象,并用每个线程的threadLcoals指向该对象
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
看到这里,你应该明白为什么ThreadLocal是线程隔离的了吧。为了让大家更清楚,我画一个图来解释。
虽然每个线程都使用了公共的ThreadLocal对象,但是我们只是将这个对象的引用作为Entry中的key而已,实际存储则是在每个线程私有的ThreadLocalMap对象中存储的。既然每个线程都有自己的ThreadLocalMap对象,线程1将数据存在线程1的ThreadLocalMap中,线程2必然访问不到。
三、ThreadLocal为什么要使用弱引用
前面我们清楚了ThreadLocal能实现线程隔离,那么为什么Entry中的key要通过弱引用指向公共的ThreadLocal对象呢?这是为了防止内存泄漏。
试想一个情景,当一个线程取出ThreadLocal中的数据,使用完后,不再需要ThreadLocal存储数据了,那我们希望JVM下次垃圾回收时,能把ThreadLocal对象回收掉。由于弱引用的存在,那么我们只需将ThreadLocal对象的引用置为null即可。看以下代码:
public class ThreadLocalDemo {
private static ThreadLocal<Person> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
//向threadLocal中存放数据
threadLocal.set(new Person());
//从threadLocal中取出数据并使用
System.out.println(threadLocal.get());
//使用完数据后,我不再需要ThreadLocal中的数据了,回收ThreadLocal对象资源
threadLocal = null;
//手动调用gc
System.gc();
//防止垃圾还没回收,线程就已经结束了
new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
//查看ThreadLocal对象是否被回收了
System.out.println(threadLocal.get());
}
static class Person{
String name = "zhangsan";
}
}
执行结果如下:
我们可以看到,当我们把指向ThreadLocal
的引用threadLocal
置为null
后,JVM垃圾回收就会回收ThreadLocal对象。那么这和弱引用又有什么关系呢?
我们假设Entry中的key是一个强引用指向ThreadLocal对象,如下图所示:
那么当我们想回收ThreadLocal
对象时,我们将指向ThreadLocal
对象的引用tl
置为空,此时key
依然是强引用指向了我们想回收的ThreadLocal
对象。文章开头我们介绍过强引用,JVM宁可报OOM都不会回收强引用指向的对象,所以ThreadLocal对象永远不会被JVM回收,该对象所占用的堆内存永远都不会得到释放。
四、使用完ThreadLocal后为什么总是要调用remove()方法
调用remove()方法和前面使用弱引用一样,也是为了防止内存溢出,只不过它们针对的对象不同。我们首先看一下ThreadLocal
中remove()
方法的源码:
public void remove() {
//获取当前线程所对应的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//若map不为空,则删除key=ThreadLocal所对应的value。这里调用了ThreadLocalMap中的remove()方法
m.remove(this);
}
我们 再看一下ThreadLocalMap
中的remove()
方法的源码:
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
//这里是计算key在table数组中的位置索引。计算方法与hashmap中的一样
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
//找到key所对应的Entry后,清除整个Entry对象
e.clear();
expungeStaleEntry(i);
return;
}
}
}
也就是说,调用ThreadLocal
中的remove()
方法,会清除ThreadLocalMap
中key=ThreadLocal对象引用的Entry
,这样做是为了保证value
中所指向的对象能够被回收。
如果使用完ThreadLocal
,我们没有调用remove()
方法,那么ThreadLocalMap
中的数据会一直存在,直到线程结束。即使我们回收了ThreadLocal
对象,也会如此,如下图:
Entry中的value应该是Person对象的强引用,而此时我们已经不需要Person对象了,JVM并不能回收强引用所指向的对象。
五、 总结
总结一下:
- ThreadLocalMap:实现了ThreadLocal的线程隔离。
- 弱引用:避免了ThreadLocal对象导致的内存泄漏
- 调用remove()方法:避免了value强引用导致不需要的数据无法被回收,从而导致内存泄漏。