当前位置: 首页 > article >正文

ThreadLocal源码解析

文章目录

  • 一、概述
  • 二、get()方法
  • 三、set()方法
  • 四、可能导致的内存泄漏问题
  • 五、remove
  • 六、思考:为什么要将ThreadLocalMap的value设置为强引用?


一、概述

  ThreadLocal是线程私有的,独立初始化的变量副本。存放在和线程进行绑定的ThreadLocalMap中。ThreadLocalMap的内部类Entry,是一个键值对的结构,key是ThreadLocal对象,value是某个变量在某个时刻的副本。并且Entry继承了WeakReference类,使其key(ThreadLocal对象)成为弱引用,如果未正确使用remove方法,可能会导致内存泄漏问题。
在这里插入图片描述构造entry对象时,key使用父类的方法,被包装成弱引用。

  ThreadLocalMapThreadLocal的一个静态内部类:
在这里插入图片描述  但是其初始化的操作,是绑定在每个线程中的,作为线程对象的属性,随着线程对象的加载而初始化。
在这里插入图片描述  并且ThreadLocalMap的内部,是一个entry键值对数组的形式。也有初始容量和扩容机制。这一点和HashMap类似,区别在于,处理Hash冲突时,HashMap使用的是拉链法,也就是对于Hash值相同的key,会形成一条链表乃至树化。而ThreadLocalMap使用的是开放定址法中的线性探测再散列,即某一个key计算出的Hash值,该位置已经有了元素,则会沿着数组下标依次向后寻找空位。
在这里插入图片描述在这里插入图片描述线性探测再散列
  ThreadLocal通常会作为类的属性,并且用static关键字修饰。原因在于ThreadLocal需要从属于某个类,而不是具体的实例。


二、get()方法

  在get方法中,主要做了几件事:

  1. 获取ThreadLocalMap。
  2. ThreadLocalMap不为空,则获取ThreadLocalMap的Entry 对象,并且对象不为空,就返回该Entry 对象的value。
  3. ThreadLocalMap为空,就执行初始化操作。
    public T get() {
    		 //获取当前线程对象(调用的是本地方法)
        Thread t = Thread.currentThread();
        //根据线程对象,获取到与该线程一一对应的ThreadLocalMap(ThreadLocalMap 是线程对象的属性) 
        ThreadLocalMap map = getMap(t);
        //map第一次是为空的
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //执行初始化操作
        return setInitialValue();
    }

  执行初始化操作:

    private T setInitialValue() {
    		 //一、初始化value为null
        T value = initialValue();
        //获取当前线程对象
        Thread t = Thread.currentThread();
        //二、用当前线程对象,获取ThreadLocalMap 
        ThreadLocalMap map = getMap(t);
        //map不为空,就将当前的ThreadLocal对象作为key,null作为value,构造Entry对象。
        if (map != null)
            map.set(this, value);
        else
        		//三、否则初始化map
            createMap(t, value);
        //返回null
        return value;
    }

	 //一
    protected T initialValue() {
        return null;
    }
    
    //二
    ThreadLocalMap getMap(Thread t) {
    		 //ThreadLocal.ThreadLocalMap threadLocals = null;
        return t.threadLocals;
    }

	 //三
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

三、set()方法

  set方法和get方法大同小异,也是根据当前线程获取ThreadLocalMap ,然后判空,执行set操作还是初始化操作。

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
        	  //一、向ThreadLocalMap 的entry中插入元素的操作。
            map.set(this, value);
        else
            createMap(t, value);
    }

四、可能导致的内存泄漏问题

  先看一下这段代码,声明了一个线程池,以及LocalVariable 静态内部类,其中的成员变量是5M大的数组。并且还有一个ThreadLocal属性,将LocalVariable作为key包装成了弱引用。

public class ThreadLocalMemoryLeak {
    private static final int TASK_LOOP_SIZE = 500;

    /*线程池*/
    final static ThreadPoolExecutor poolExecutor
            = new ThreadPoolExecutor(5, 5, 1,
            TimeUnit.MINUTES,
            new LinkedBlockingQueue<>());

    static class LocalVariable {
        private byte[] a = new byte[1024 * 1024 * 5];/*5M大小的数组*/
    }

    ThreadLocal<LocalVariable> threadLocalLV;

    public static void main(String[] args) throws InterruptedException {
        SleepTools.ms(4000);
        for (int i = 0; i < TASK_LOOP_SIZE; ++i) {
            poolExecutor.execute(new Runnable() {
                public void run() {
                    SleepTools.ms(500);
//
//                    LocalVariable localVariable = new LocalVariable();
//
//
//                    ThreadLocalMemoryLeak oom = new ThreadLocalMemoryLeak();
//                    oom.threadLocalLV = new ThreadLocal<>();
//                    oom.threadLocalLV.set(new LocalVariable());
//
//                   oom.threadLocalLV.remove();

                    System.out.println("use local varaible");

                }
            });

            SleepTools.ms(100);
        }
        System.out.println("pool execute over");
    }

}

  如果没有加入ThreadLocal,而是仅仅在空跑,使用jvisualvm进行观察,看到的内存使用情况,是相对比较平稳的:
在这里插入图片描述  接着打开注释:

  ThreadLocalMemoryLeak oom = new ThreadLocalMemoryLeak();
  oom.threadLocalLV = new ThreadLocal<>();
  oom.threadLocalLV.set(new LocalVariable());

  发现在运行过程中,内存使用情况起伏明显,多次触发gc。
在这里插入图片描述  并且最终程序执行完成后,内存还处于较高的水平,也就是说明堆中还存在很多没有被回收的垃圾对象。
在这里插入图片描述  为什么和没有使用ThreadLocal之前,会有如此大的差距?原因在于,每次垃圾回收时,作为弱引用的Entry的key:ThreadLocal对象会被回收,但是其value没有被回收。(在JVM停止时统一销毁)。
在这里插入图片描述  而每个线程中都存在一个5M的强引用对象没有被回收。
  解决方式是,在ThreadLocal使用完成后,手动调用remove方法进行清除:

    public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

五、remove

  在remove方法中,主要完成了三件事:

  1. 获取哈希表。
  2. 计算索引,根据 key(即 ThreadLocal 对象)的哈希码,计算它在哈希表中的索引。
  3. 遍历表格中的链表查找匹配的条目。

  而expungeStaleEntry方法中,除了将value和entry的引用全部置空以外,还会继续向后扫描,将ThreadLocalMap中弱引用的key已经被回收的entry的value置空。(get和set方法中也有该实现)

        private void remove(ThreadLocal<?> key) {
            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)]) {
                if (e.get() == key) {
                		 //将key的引用置空
                    e.clear();
                    //一
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

			 //一
			 private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            //将指定下标的value的引用置空
            tab[staleSlot].value = null;
            //将指定下标的entry置空
            tab[staleSlot] = null;
            //table的长度减少
            size--;

            //这部分代码会继续扫描从 staleSlot 后的条目。如果遇到 ThreadLocal 对象已经被回收(k == null),则清除该条目。
            //否则,重新计算哈希位置并尝试将条目移动到新的位置。
            //这里使用了一个 while 循环来处理条目的重新定位,确保哈希表在移除过期条目后依然保持正确。
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

六、思考:为什么要将ThreadLocalMap的value设置为强引用?

  线程本地存储的一个核心需求是,数据必须与线程的生命周期绑定,直到该线程结束或者显式移除该值。如果线程本地存储的 value 被弱引用,就无法保证它在使用时的可用性,可能会导致意外的回收和不可预期的行为。如果 ThreadLocalMap 的 value 被设置为弱引用,那么 ThreadLocalMap 中的条目就可能会在垃圾回收时被回收,因为 value 被弱引用(即没有强引用指向它)。这就可能导致在需要访问该值时,数据已经被清理掉。
  最主要的点在于,ThreadLocalMap 的生命周期跟 Thread 一样长。



http://www.kler.cn/a/526308.html

相关文章:

  • 留学生scratch计算机haskell函数ocaml编程ruby语言prolog作业VB
  • Qt中json的使用
  • 初始化mysql报错cannot open shared object file: No such file or directory
  • 电商系统-用户认证(三)基于公钥解析JWT令牌
  • QPS 值是怎样进行计算和应用的
  • 解锁微服务:五大进阶业务场景深度剖析
  • 5.3.2 软件设计原则
  • [JavaWeb]搜索表单区域
  • Windows11暂停自动更新
  • (二)PosrgreSQL: Python3 连接Pgvector出错排查
  • 巴塞尔问题详解:计算所有正整数平方的倒数之和
  • DeepSeek R1本地部署详细指南
  • Java 性能优化与新特性
  • [OO ALV] OO ALV 基础显示
  • allegro修改封闭图形线宽
  • 独立成分分析 (ICA):用于信号分离或降维
  • wordpress外贸独立站常用询盘软件
  • Rust语言进阶之enumerate用法实例(九十六)
  • 第33篇:Python开发进阶:自然语言处理与文本分析
  • Java继承中的静态方法隐藏与实例变量隐藏:深入解析与最佳实践
  • 年化19.3%策略集|ctpbee_api替换成openctp整合backtrader实盘方案(代码+数据)
  • 大厂面试题备份20250129
  • dify实现原理分析-rag-检索(Retrieval)服务的实现
  • 信号处理以及队列
  • 一文讲解Java中的异常处理机制
  • 变量和简单数据类型(字符串)