likes
comments
collection
share

讲透ThreadLocal

作者站长头像
站长
· 阅读数 12

ThreadLocal 很容易让人望文生义,想当然地认为是一个本地线程,实际上这是一种错误的理解。实际上ThreadLocal 并不是一个 Thread而是 Thread 的一个局部变量,这点从源码中就能看出来。

public class Thread implements Runnable {
   
    /* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}    

  当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程 提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

  ThreadLocal 既然是线程局部变量,那么理所当然就应该存储在自己的线程对象中,从源码中可以看到线程局部变量是存储在 Thread 对象的 threadLocals 属性中,而 threadLocals 属性是一个 ThreadLocal.ThreadLocalMap 对象。

一、Thread 、ThreadLocal 和 ThreadLocalMap 之间的关系?

  • Thread 中的 threadLocals 属性就是 ThreadLocal.ThreadLocalMap
  • ThreadLocalMapThreadLocal静态内部类
public class ThreadLocal<T> {
  
    // 省略部分代码 ...
    
    static class ThreadLocalMap {
        // 静态内部类
        static class Entry extends WeakReference<ThreadLocal<?>> {
        
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        // ThreadLocalMap 的初始容量   
        private static final int INITIAL_CAPACITY = 16;
        
        private Entry[] table;
        
        private int size = 0;
   }
   
   // 省略部分代码 ...
}   

通过这段代码可以画出如下关系图:

讲透ThreadLocal

总结:一个 Thread 中只有一个 ThreadLocalMap ,一个 ThreadLocalMap 中可以有多个ThreadLocal 对象,其中一个 ThreadLocal 对象对应一个 ThreadLocalMap 中的一个 Entry

1. ThreadLocalMap 中 Entry 为什么要继承 WeakReference ?

在 Java 里面存在着强引用、弱引用、软引用和虚引用

A a = new A();
B b = new B();

// 考虑这样的情况
C c = new C(b);
b = null;

对于这段代码,考虑 GC 的情况。要知道 b 被置为 null,那么是否意味着一段时间后 GC 可以回收 b 所分配的内存空间呢?

答案是否定的,因为即便 b 被置为 null,但是 c 仍然持有对 b 的引用,而且还是强引用,所以 GC 不会回收 b 原先所分配的空间。导致既不能回收利用,又不能使用,这就造成了内存泄露

这种情况下该如何防止内存泄漏呢?

可以让 c = null 或者 使用弱引用,如 WeakReference w = new WeakReference(b)

所以根据 Thread 、ThreadLocal 和 ThreadLocalMap 之间的关系,可以分析出他们在堆栈内存中的引用关系如下图:

讲透ThreadLocal

所以 Entry 继承 WeakReference 是为了防止内存泄漏的处理方式而已

  • ThreadLocal 使用到了弱引用,是否意味着不会存在内存泄露呢?

如果把 ThreadLocal 置为 null,那么意味着堆内存中的 ThreadLocal 实例不再有强引用指向,只有弱引用存在,因此 GC 是可以回收这部分空间的,也就是key是可以回收的。但是 value 却存在一条从Thread 对象过来的强引用。因此只有当 Thread 销毁时,value才能得到释放。

所以只要这个 Thread 对象被 GC 回收,就不会出现内存泄露,但在 ThreadLocal 设为 null 和线程结束这段时间内不会被回收的,就发生了我们认为的内存泄露。更重要的是在使用线程池的时候,线程结束是不会销毁的,再次使用的,就可能出现内存泄露。

  • 那么 ThreadLocal 中是如何避免这种情况发生的呢?

ThreadLocalMap 中的 set/getEntry 方法中,会对 key 为 null(即是 ThreadLocal 为 null)进行判断,如果为null的话,那么是会对 value 置为 null。同时可以通过调用 ThreadLocal 的remove 方法进行释放

注意: ThreadLocal 在没有使用线程池时,正常情况下不会存在内存泄露。但是如果使用了线程池的话,就依赖于线程池的实现,如果线程池不销毁线程的话,那么就会存在内存泄露。所以我们在使用线程池的时候,使用 ThreadLocal 要格外小心!

二、ThreadLocal 是如何做到为每一个线程维护变量副本的呢?

通过上面的分析我们知道了在 ThreadLocal 类中定义了 ThreadLocalMap,用于存储每一个线程的局部变量,ThreadLocalMap 的键值是 ThreadLocal,值为线程局部变量

1.ThreadLocal 的 set 方法

讲透ThreadLocal

由于每个 Thread 实例都有一个 ThreadLocalMap,所以在进行 set 的时候,首先根据Thread.currentThread() 获取当前线程,然后根据当前线程t,调用 getMap(t) 获取 ThreadLocalMap 对象。如果是第一次设置值,ThreadLocalMap 对象是空值,所以会进行初始化操作,即调用createMap(t,value)方法。

讲透ThreadLocal

可以看到 set(T value) 方法为每个 Thread 对象都创建了一个 ThreadLocalMap,并且将 value 放入 ThreadLocalMap 中。

2.ThreadLocal 的 get 方法

讲透ThreadLocal 讲透ThreadLocal

首先获取 ThreadLocalMap 对象,由于 ThreadLocalMap 使用的当前的 ThreadLocal 作为key,所以传入的参数为 this,然后调用 getEntry 方法,通过这个key构造索引,根据索引去 table(Entry数组)中去查找线程本地变量,根据下标找到 Entry 对象,然后判断 Entry 对象e不为空 并且 e的引用与传入的key一样则直接返回,如果找不到则调用 getEntryAfterMiss 方法。调用 getEntryAfterMiss 表示直接散列到的位置没找到,那么顺着hash表循环地往下找,从i开始,一直往下找,直到出现空的槽为止。

讲透ThreadLocal

三、ThreadLocal 如何回收内存?

1. 在 ThreadLocal 层面的内存回收

当线程死亡时,那么所有的保存在的线程局部变量就会被回收,其实这里是指线程 Thread 对象中的 ThreadLocal.ThreadLocalMap threadLocals 会被回收

2. ThreadLocalMap 层面的内存回收

如果线程可以活很长的时间,那么就涉及到在线程的生命期内如何回收 ThreadLocalMap 的内存了,不然的话ThreadLocalMap保存的 Entry 对象越多,那么 ThreadLocalMap 就会越来越大,占用的内存就会越来越多,所以对于已经不需要了的线程局部变量,就应该清理掉其对应的 Entry 对象。

为了解决这个问题,Entry 对象的 key 是 WeakReference 的包装,当 ThreadLocalMapprivate Entry[] table,已经被占用达到了三分之二时 threshold = 2/3(也就是线程拥有的局部变量超过了10个) ,就会尝试回收 Entry 对象,我们可以看到 ThreadLocalMap.set()方法中有下面的代码

讲透ThreadLocal