Java ThreadLocal 源码全解

文章目录

  • 1. ThreadLocal 使用
  • 2. ThreadLocal 原理
        • 关系图解:
  • 3. ThreadLocal 可能导致内存泄露
      • 3.1. ThreadLocal 在 JVM 内存中的引用关系
      • 3.2. ThreadLocalMap 中内存泄漏分析
      • 3.3. ThreadLocalMap 中对内存泄漏的应对

1. ThreadLocal 使用

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

public static void main(String[] args) {
        ThreadLocal threadLocal = new ThreadLocal(); // 创建线程本地变量
        threadLocal.set("It is main thread"); // 主线程设置数据
        new Thread(() -> {
            threadLocal.set("It is thread1"); // 子线程1设置数据
            System.out.println(threadLocal.get().toString());
        }).start();
        new Thread(() -> {
            threadLocal.set("It is thread2"); // 子线程2设置数据
            System.out.println(threadLocal.get().toString());
        }).start();
        System.out.println(threadLocal.get());
    }

运行结果:

It is thread1
It is main thread
It is thread2

子线程1率先打印了ThradLocal 变量存储的数据, 之后主线程运行,打印其ThradLocal 变量存储的数据,可以看出虽然多个线程对同一个ThradLocal变量进行设置,但是 ThreadLocal 中存储的数据并没有相互影响.

2. ThreadLocal 原理

此处以Jdk 1.8 中 ThreadLocal 的get() 方法源码来分析原理.

public T get() {
        Thread var1 = Thread.currentThread(); 
        ThreadLocal.ThreadLocalMap var2 = this.getMap(var1);
        if (var2 != null) {
            ThreadLocal.ThreadLocalMap.Entry var3 = var2.getEntry(this);
            if (var3 != null) {
                Object var4 = var3.value;
                return var4;
            }
        }
        return this.setInitialValue();
    }

get()方法完成了以下事情:

  1. 调用Thread.currentThread()获取当前线程对象;

  2. 根据当前线程对象,调用getMap(Thread)获取线程对应的ThreadLocalMap对象:

    ThreadLocal.ThreadLocalMap getMap(Thread var1) {
         return var1.threadLocals; // threadLocals是Thread类的成员变量,初始化为null.
    }
    
  3. 如果获取的map不为 null,则在map中以当前 ThreadLocal 对象作为key去map中获取对应的键值对Entry.获取的map为 null 则说明当前线程 ThreadLocalMap 还未初始化,需要进入步骤5.

  4. 若键值对 Entry 不为null,则返回其中存储的value值,否则进入步骤5.

  5. 调用setInitialValue()方法,对线程的 ThreadLocalMap 对象进行初始化操作,ThreadLocalMap对象保存的 Entry 的key为 ThreadLocal 实例对象,value为initialValue()方法的返回值.

从上面的分析可以看到 ThreadLocal 的实现依赖于ThreadLocalMap类, 而ThreadLocalMap 是 ThreadLocal的静态内部类.每个Thread维护一个ThreadLocalMap映射表,这个映射表中存储的键值对Entry的key为ThreadLocal对象本身,value则是需要存储的Object. 由此也可知一个ThreadLocal实例只能存储一条数据,不过线程中维护的ThreadLocalMap表可以存储多个Entry, 故一个线程可以通过多个ThreadLocal实例来存储多个本地变量.

关系图解:

Java ThreadLocal 源码全解_第1张图片

3. ThreadLocal 可能导致内存泄露

3.1. ThreadLocal 在 JVM 内存中的引用关系

ThreadLocal 在 JVM 内存中的引用关系如下图所示, 实线为强引用,虚线为弱引用
Java ThreadLocal 源码全解_第2张图片
其中Entry的定义如下,可知ThreadLocalMap是使用 ThreadLocal 的弱引用作为Key

static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> var1, Object var2) {
                super(var1);
                this.value = var2;
            }
        }

3.2. ThreadLocalMap 中内存泄漏分析

如我们所知,弱引用对象在Java虚拟机进行垃圾回收时就会被释放, 那就有必要考虑以下问题:

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用关联,那么在虚拟机进行垃圾回收时,这个ThreadLocal会被立即回收.此时ThreadLocalMap中就会出现key为null的Entry,这些key对应的value也就无法访问了. 但是value却存在一条从Current Thread过来的强引用链,因此只有当Current Thread销毁时,value占用的内存才能得到回收.

强引用链如下:
CurrentThread Ref -> Thread -> ThreadLocalMap -> Entry -> value

通常情况下新建线程完成任务后肯定是要被销毁的,这样也就没什么问题.但在线程对象不被回收的情况下,例如使用线程池时,核心线程是一直在运行的,线程对象不会被回收, 那就可能出现内存泄露的问题.

3.3. ThreadLocalMap 中对内存泄漏的应对

ThreadLocalMap 的设计中已经考虑到这种情况, 其对应的方案是在 ThreadLocal 的get(),set(),remove()方法调用的时候都会清除线程 ThreadLocalMap 里所有key为null的value.

private int expungeStaleEntry(int var1) {
            ThreadLocal.ThreadLocalMap.Entry[] var2 = this.table;
            int var3 = var2.length;
            var2[var1].value = null;  // var1为 Entry 数组中 key 为 null 的Entry的下标
            var2[var1] = null;
            --this.size;
           ......
        }

但这些被动的预防措施并不能保证不会内存泄漏:

  1. 分配使用了ThreadLocal的线程在线程池中不销毁也不再被使用,那么 value 占用的这块内存一直存在,会导致内存泄漏.
  2. 分配使用了 ThreadLocal 的线程在之后的任务中不再使用线程本地变量,也就是不调用get(),set(),remove()方法,那么value 占用的这块内存依然不会被释放回收,存在内存泄漏.

你可能感兴趣的:(Java,基础,Java,并发)