文章目录
1.前言
前几天面试面试官问到和ThreadLocal相关的一些问题,回来以后对相关问题进行了详细的思考并认真阅读了一下ThreadLocal源码,以前觉得ThreadLocal只是一个用来做线程变量隔离的类,看完源码发现这个类挺有意思的,里面涉及的原理还是比较复杂,特此做一下记录。
2.内存泄漏
面试官:Java语言会发生内存泄漏吗?谈谈你的理解
当时问到这样一个问题首先第一反应就是ThreadLocal类,毕竟早就听说过ThreadLocal的内存泄漏问题,要回答这个问题,首先是需要理解什么是内存泄漏。
什么是内存泄漏?
内存泄漏是指我们在写程序的时候开辟使用了一块内存区域,在这块内存区域使用完毕后,该区域的内容依然占用着内存,并且一直占着不会发生回收,这样就称为内存泄漏。这样将会导致资源浪费、频繁发生GC等问题。
内存泄露的本质
Java中造成内存泄漏的原因有很多,归结原因本质上都是由于资源已经使用完毕,但是该资源却被其他对象或变量引用着导致无法回收该资源。导致内存泄漏的原因有很多,ThreadLocal类只是Java其中一个例子。
3.ThreadLocal
ThreaLocal介绍
面试官:你刚才说到ThreadLocal,那你简单介绍一下ThreadLocal类以及用法吧
ThreadLocal从字面上看可以理解成线程的本地变量,线程中使用ThreadLocal填充的数据只属于当前线程,其他线程不能够使用,起到与其他线程的隔离作用。通过ThreadLocal对象的get和set方法可以设置和提取出改线程中设置的值。
ThreadLocal在类注释中给出的官方代码示例如下:
public class ThreadId {
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0);
// Thread local variable containing each thread's ID
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
// Returns the current thread's unique ID, assigning it if necessary
public static int get() {
return threadId.get();
}
}
这段代码的作用是在调用get()的时候给当前线程生成唯一的id并且存在ThreadLocal对象threadId中,看完这段代码后可能还是很懵,下面用一个简单示例来演示ThreadLocal的使用:
public static void main(String[] args) {
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(() -> {
threadLocal.set(finalI * 100);
try {
TimeUnit.MILLISECONDS.sleep(100);
System.out.println("当前线程["+Thread.currentThread().getName()+"]的值:"+threadLocal.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
},String.valueOf(finalI)).start();
}
}
这段代码启动了10个线程,线程名字为0到9,并且通过ThreadLocal的set将线程i编号乘100进行存放,最后打印出对应的线程和值,打印结果:
当前线程[7]的值:700
当前线程[6]的值:600
当前线程[1]的值:100
当前线程[8]的值:800
当前线程[5]的值:500
当前线程[2]的值:200
当前线程[3]的值:300
当前线程[9]的值:900
当前线程[4]的值:400
当前线程[0]的值:0
可以看到在代码中我们并没有使用和线程有关的变量,ThreadLocal对象仍然能够正确的获取到当前线程存放的值。
ThreaLocal原理
面试官:你解释一下ThreadLocal类是如何存放这些和线程有关的值呢?
ThreadLocal中的方法不多,常用的就set(),get(),remove(),接下来我们从这几个方法的源码层面来看ThreadLocal的原理。
1. ThreadLocal类结构
从类结构可以看到在ThreadLocal类中有个内部类ThreadLocalMap,ThreadLocalMap中还有一个内部类Entry。
2. 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);
}
从set方法源码中可以看到先从当前调用线程中获取到ThreadLocalMap,这个ThreadLocalMap在后面详细讲解,这里就先简单理解成一个key是ThreadLocal,value是待存入值的kv键值对(其实也差不多)。拿到ThreadLocalMap后,如果ThreadLocalMap不为空,就调用ThreadLocalMap.set()进行存储,如果为空则创建一个新的ThreadLocalMap对象。创建ThreadLocalMap对象通过new直接创建即可。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
3. ThreadLocal.get()方法
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();
}
- get()首先从当前线程中取出ThreadLocalMap对象,ThreadLocalMap存在Thread对象的threadLocals变量中,每个线程有自己的threadLocals变量
- 然后再从ThreadLocalMap中获取Entry对象
- 如果Entry对象不为空,直接从Entry中取出value值并返回
- 如果ThreadLocalMap为空或者Entry为空,调用setInitialValue()方法初始化ThreadLocalMap
4. ThreadLocal.remove()方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
remove()方法比较简单,先根据当前线程获取ThreadLocalMap对象,再判断ThreadLocalMap对象是否为空,不为空则移除该key对应kv键值对即可。
从上面的代码可以看出,对于ThreadLocal类的操作都是基于ThreadLocalMap进行的,所以真正存放变量的地方是在ThreadLocalMap类上。
4.ThreadLocalMap原理
面试官:你说ThreadLocal是基于ThreadLocalMap来存储的,那么ThreadLocalMap是怎么存储的呢?
ThreaLocalMap介绍
ThreadLocalMap是一个在ThreadLocal内定义的哈希映射,只适合维护线程本地值,ThreadLocalMap中所有方法都是私有的,其结构与HashMap比较类似ThreadLocalMap的默认初始化大小也是16。在ThreadLocalMap中以Entry对象数组进行数据存储,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类中只有一个成员变量value用于存放实际值,并且该类继承了WeakReference对象,并在构造时调用WeakReference的构造方法,将Entry的key(ThreadLocal对象)设置为弱引用。而ThreadLocalMap又是由多个Entry对象构成的,其结构图如下:
ThreadLocalMap中存放的Entry数组其实可以是一个环型的,因为在ThreadLocalMap调用set()时会调用nextIndex()方法寻找槽位,当i+1大于数组长度时下一个索引就指向0。同样的prevIndex()方法寻找上一索引,当寻找的上一位小于零时,索引指向数组最后一位。
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
ThreaLocalMap设置值
ThreaLocalMap设置值主要是通过ThreadLocalMap.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();
}
1. hash的计算
索引通过 int i = key.threadLocalHashCode & (len-1);计算得到。首先计算当前ThreadLocal对象的hash值,通过一个AtomicInteger对象加上HASH_INCREMENT(0x61c88647),相加后的值与(len-1)进行与运算得到索引值,该值一定是小于len的。
在for循环中从tab[]数组中的i+1位拿Entry对象,例如计算得到的索引i为2,则在for循环中从数组第3位开始获取。
2. 设置的过程
拿到Entry对象后判断这个Entry对象,如果为空,跳出循环,并在当前位置新增一个Entry;
如果不为空则通过get()方法从WeakReference中取出存放的ThreadLocal对象k。
得到k之后进行判断,如果k不为空且k和传入的ThreadLocal不同则从下一个槽位寻找,若k和传入的ThreadLocal相同则直接重置value;
若k和key不相同,则寻找下一个槽位,继续判断,下一个槽位为空则跳出循环放入
若k为空,则表示这个位置的ThreadLocal对象被回收,调用replaceStaleEntry()方法顶替这个空的位置避免内存泄漏。
5.ThreadLocal内存泄漏
面试官:那你能解释一下为什么ThreadLocal会发生内存泄漏吗?
现在都知道ThreadLocal会发生内存泄漏问题,那么ThreadLocal为什么会发生内存泄漏呢?这就要从ThreadLocal的持有对象上来说了,下面示例代码
public static void main(String[] args) {
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
ThreadLocal<Integer> threadLocal3 = new ThreadLocal<>();
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(() -> {
threadLocal.set(finalI * 1);
threadLocal2.set(finalI * 10);
threadLocal3.set(finalI * 100);
System.out.println("Thread " + Thread.currentThread().getName() + "\tset\t" + finalI);
try {
Thread.sleep(1000);
System.out.println("Thread " + Thread.currentThread().getName() + "\tget:\t" + threadLocal.get());
System.out.println("Thread " + Thread.currentThread().getName() + "\tget:\t" + threadLocal2.get());
System.out.println("Thread " + Thread.currentThread().getName() + "\tget:\t" + threadLocal3.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
这里创建三个ThreadLocal对象,启动五个线程,在不同线程中设置ThreadLocal对象的值,
打印结果如下
Thread 0 set 0
Thread 4 set 4
Thread 3 set 3
Thread 1 set 1
Thread 2 set 2
Thread 3 get: 3
Thread 4 get: 4
Thread 4 get: 40
Thread 4 get: 400
Thread 2 get: 2
Thread 1 get: 1
Thread 0 get: 0
Thread 1 get: 10
Thread 2 get: 20
Thread 3 get: 30
Thread 2 get: 200
Thread 1 get: 100
Thread 0 get: 0
Thread 0 get: 0
Thread 3 get: 300
从打印结果中可以看出,在不同的线程线程中可以设置和读取多个ThreadLocal对象设置的值,在sleep()方法处打上断点查看执行线程属性时,当前线程中有一个threadLocals属性,该属性类型为ThreadLocalMap,此时这个ThreadLocalMap存放的Entry数为3个,分别在1、8、10当中。
我们知道内存泄漏是发生在ThreadLocalMap的内部类Entry上,因为继承了WeakReference弱引用,而用于存Entry的key也就是ThreadLocal对象,当发生GC时,弱引用的对象将会被回收即Entry的key被回收变为null。但是整个Entry对象却一直被持有没有被回收,所以该Entry一直占着空间却无法被获取(key为null),从而导致内存泄漏问题。
面试官:你确定内存泄漏是由于弱引用而造成的吗?
显然,弱引用只是造成内存的诱导因素,但不是根本原因。下图为一个Entry对象的引用链路图:
从图上可以看到一个Entry对象被Thread的中的threadLocals(ThreadLocalMap)持有以及ThreadLocal对象指向Entry的key,当ThreadLocal对象被回收,即图中虚线部分引用失效,此时如果线程结束,则Entry将不会有强引用指向,但是如果这个线程一直存在不释放,如线程池,那么Entry对象将一直被ThreadLocalMap强引用,但是此时Entry对象的key已被回收为null,无法根据key来获取,但是Entry对象未销毁始终会占用内存,因此发生内存泄漏的情况。
从很多地方看到ThreadLocal内存泄漏的原因是WeakReference弱引用引起的,其实根本原因并不完全是因为弱引用,相反使用WeakReference从某种程度上讲是为了解决内存占用(加快GC回收)。真正产生内存泄露的原因是线程生命周期没结束,线程未销毁导致Entry强引用始终存在。
面试官:那么,为什么要使用弱引用呢?
从上面分析可以看出,线程如果不结束将会导致ThreadLocal中的threadLocals一直持有Entry对象,如果Entry中的key使用强引用的话,那么ThreadLocal对象始终被Entry引用着,线程不结束ThreadLocal对象不会被回收。但是如果使用弱引用的话,ThreadLocal对象无论Thread是否结束依然会被回收。在不手动删除key的时候弱引用相比强引用多了一层保障,防止ThreadLocal对象占用内存。
6.总结
- ThreadLocal在多线程开发中还是比较常用的一个类,能够使用一个ThreadLocal对象在不同线程中存放当前线程专属的变量,从而达到线程隔离的目的
- ThreadLocal内部使用ThreadLocalMap进行存储
- 每一个线程内部(Thread对象)通过threadLocals存储该线程中的不同ThreadLocal对象所需要保存的值,threadLocals变量是ThreadLocalMap类型的
- ThreadLocalMap内部使用KV键值对的形式进行变量存储,key为ThreadLocal对象,value为值
- ThreadLocal在调用get()、set()、remove()后都会清理key为null的部分