> 文档中心 > 面试官一步步逼问让我对ThreadLocal有了更深入的思考

面试官一步步逼问让我对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类结构
从类结构可以看到在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的部分