面试官一步步逼问让我对ThreadLocal有了更深入的思考
文章目录
- 1.前言
- 2.内存泄漏
-
- 什么是内存泄漏?
- 内存泄露的本质
- 3.ThreadLocal
-
- ThreaLocal介绍
- ThreaLocal原理
- 4.ThreadLocalMap原理
-
- ThreaLocalMap介绍
- ThreaLocalMap设置值
- 5.ThreadLocal内存泄漏
- 6.总结
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 0set0Thread 4set4Thread 3set3Thread 1set1Thread 2set2Thread 3get:3Thread 4get:4Thread 4get:40Thread 4get:400Thread 2get:2Thread 1get:1Thread 0get:0Thread 1get:10Thread 2get:20Thread 3get:30Thread 2get:200Thread 1get:100Thread 0get:0Thread 0get:0Thread 3get: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的部分