ThreadLocal源码剖析

发布于:2025-03-11 ⋅ 阅读:(25) ⋅ 点赞:(0)


ThreadLocal的set,get方法只是作为一个接口,实质上还是对Thread里面的ThreadLocalMap里面的进行操作
在这里插入图片描述

在这里插入图片描述
jdk1.8之后,每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。

Thread t = new Thread();
Map map = getMap(t);//根据线程获取他的threadLocalMap
map.set(new ThreadLocal<xxx>(),new Object());

在这里插入图片描述

四种引用的概念

在这里插入图片描述

Entry的类定义

/*
 * Entry继承WeakReference,并且用ThreadLocal作为key.
 * 如果key为null(entry.get() == null),意味着key不再被引用,
 * 因此这时候entry也可以从table中清除。
 * entry.get()返回的是ThreadLocal类型的key
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

弱引用和内存泄漏

强引用(“Strong” Reference),就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象。

弱引用(WeakReference),垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

如果key使用强引用

假设ThreadLocalMap中的key使用了强引用,那么会出现内存泄漏吗?

​ 此时ThreadLocal的内存图(实线表示强引用)如下:
在这里插入图片描述
假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。

​ 但是因为threadLocalMap的Entry强引用了threadLocal,造成threadLocal无法被回收。

​ 在没有手动删除这个Entry以及CurrentThread依然运行的前提下,始终有强引用链 threadRef->currentThread->threadLocalMap->entry,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。

​ 也就是说,ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的。

如果key使用弱引用

在这里插入图片描述
同样假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。

​ 由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。

​ 但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry -> value ,value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏

​ 也就是说,ThreadLocalMap中的key使用了弱引用, 也有可能内存泄漏。

其实不管是强引用还是弱引用,都会漏内存,只不过是多少的问题,只要忘记remove了,强引用是漏了kv,弱引用漏了v。只能说有点改善,但是不多

为什么使用弱引用

根据刚才的分析, 我们知道了: 无论ThreadLocalMap中的key使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。

​ 要避免内存泄漏有两种方式:

  1. 使用完ThreadLocal,调用其remove方法删除对应的Entry

  2. 使用完ThreadLocal,当前Thread也随之运行结束

相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。

也就是说,只要记得在使用完ThreadLocal及时的调用remove,无论key是强引用还是弱引用都不会有问题。那么为什么key要用弱引用呢?

​ 事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。

​ 这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。
在这里插入图片描述

最主要啊,是弱引用会导致key为null,如果key为null了,就说明这个Entry过期了,就要进行垃圾回收了

hash冲突的解决

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocal.ThreadLocalMap map = getMap(t);
        if (map != null)
            //调用了ThreadLocalMap的set方法
            map.set(this, value);
        else
            createMap(t, value);
    }
    
    ThreadLocal.ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
        	//调用了ThreadLocalMap的构造方法
        t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
    }

这个方法我们刚才分析过, 其作用是设置当前线程绑定的局部变量 :

​ A. 首先获取当前线程,并根据当前线程获取一个Map

​ B. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)

(这里调用了ThreadLocalMap的set方法)

​ C. 如果Map为空,则给该线程创建 Map,并设置初始值

(这里调用了ThreadLocalMap的构造方法)

这段代码有两个地方分别涉及到ThreadLocalMap的两个方法, 我们接着分析ThreadLocalMap这两个方法。

 /*
  * firstKey : 本ThreadLocal实例(this)
  * firstValue : 要保存的线程本地变量
  */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        //初始化table
        table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
        //计算索引(重点代码)
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        //设置值
        table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
        size = 1;
        //设置阈值
        setThreshold(INITIAL_CAPACITY);
    }

构造函数首先创建一个长度为16的Entry数组,然后计算出firstKey对应的索引,然后存储到table中,并设置size和threshold。

a. 关于firstKey.threadLocalHashCode

private final int threadLocalHashCode = nextHashCode();
    
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
//AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减,适合高并发情况下的使用
    private static AtomicInteger nextHashCode =  new AtomicInteger();
     //特殊的hash值
    private static final int HASH_INCREMENT = 0x61c88647;

这里定义了一个AtomicInteger类型,每次获取当前值并加上HASH_INCREMENT,HASH_INCREMENT = 0x61c88647,这个值跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里, 也就是Entry[] table中,这样做可以尽量避免hash冲突。每多一个ThreadLocal,都递增一次HASH_INCREMENT

b. 关于& (INITIAL_CAPACITY - 1)

​ 计算hash的时候里面采用了hashCode & (size - 1)的算法,这相当于取模运算hashCode % size的一个更高效的实现。正是因为这种算法,我们要求size必须是2的整次幂,这也能保证在索引不越界的前提下,使得hash发生冲突的次数减小。

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;
            }
        }
        // map为null,初始化map 或者是e为null,元素不存在,给他初始化为null
        return setInitialValue();
    }
     private Entry getEntry(ThreadLocal<?> key) {
	    int i = key.threadLocalHashCode & (table.length - 1);
	    Entry e = table[i];
	    if (e != null && e.get() == key)
	        return e;
	    else
	        return getEntryAfterMiss(key, i, e);
     }
     private T setInitialValue() {
        T value = initialValue();//这里是直接返回null,保证了key不存在时,get这个key会对这个位置的value设置为null
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);//保证了key不存在时,get这个key会对这个位置的value设置为null
        else
            createMap(t, value);
        return value;
    }
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }
    protected T initialValue() {
        return null;
    }

ThreadLocalMap中的set方法

private void set(ThreadLocal<?> key, Object value) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        //计算索引(重点代码,刚才分析过了)
        int i = key.threadLocalHashCode & (len-1);
        /**
         * 使用线性探测法查找元素(重点代码)
         */
        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
            //ThreadLocal 对应的 key 存在,直接覆盖之前的值
            if (k == key) {
                e.value = value;
                return;
            }
            // key为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,
           // 当前数组中的 Entry 是一个陈旧(stale)的元素
            if (k == null) {
                //用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏
                replaceStaleEntry(key, value, i);
                return;
            }
        }
    
    	//ThreadLocal对应的key不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的Entry。
            tab[i] = new Entry(key, value);
            int sz = ++size;
            /**
             * cleanSomeSlots用于清除那些e.get()==null的元素,
             * 这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null。
             * 如果没有清除i往后任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行				 
             *  rehash(执行一次全表的扫描清理工作)
             */
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
}

 /**
     * 获取环形数组的下一个索引
     */
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }

代码执行流程:

  • A. 首先还是根据key计算出索引 i,然后查找i位置上的Entry,

  • B. 若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值,

  • C. 若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry,

  • D. 不断循环检测,直到遇到为null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1。

​ 最后调用cleanSomeSlots,清理key为null的Entry,最后返回是否清理了Entry,接下来再判断sz 是否>= thresgold达到了rehash的条件,达到的话就会调用rehash函数执行一次全表的扫描清理。

重点分析 : ThreadLocalMap使用线性探测法来解决哈希冲突的。

​ 该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。

​ 举个例子,假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。

​ 按照上面的描述,可以把Entry[] table看成一个环形数组。

ThreadLocal的remove

因为ThreadLocalMap是基于线性探测法(开放寻址法)而非拉链法,所以循环条件是e!=null,hash出来位置i,往后遍历是否找到了key,找到了就清理i这个位置过期条目,并且从这个位置开始进行连续段的内存清理

        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) {
                    e.clear();
                    //从i开始清理过期Entry,过期的回收掉,没过期的Entry就rehash到新的位置,
                    //直到遍历到Entry为null的位置
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

演示垃圾回收

在这里插入图片描述
在这里插入图片描述

get触发GC

get(c)触发GC,恰好是要获取1位置的value,↓↓↓↓
在这里插入图片描述
get(c)之后,原来key为null,value为1,现在key为c,value为null

在这里插入图片描述

set触发GC

在这里插入图片描述
清理完的效果
在这里插入图片描述
针对以上探讨的情况,我们推荐使用remove来清理内存,set,get的方式比较局限

ThreadLocal-内存清理

英文解释:
Expunge:清理,清除
Stale:过期的,失效的

探测式清理(ExpungeStaleEntry)

从i开始清理过期Entry,过期的回收掉,没过期的Entry就rehash到新的位置,直到遍历到Entry为null的位置
ExpungeStaleEntry返回下一个Entry为null的位置i
在这里插入图片描述

启发式清理(cleanSomeSlots)

Slot:槽,就是哈希槽的意思
清理 l o g 2 n log_{2}n log2n次,
在这里插入图片描述
cleanSomeSlot的调用背景是基于set
在这里插入图片描述

ReplaceStaleEntry

替换旧的无效节点
目的在于基于staleSlot位置,往前找,直到遇到第一个Entry为null的位置,往前找的过程中记录最前面过期Entry的位置slotToExpunge
往前找完了之后往StaleSlot后面找,直到遇到第一个Entry为null的位置,往后找的过程中清理所有过期Entry

在这里插入图片描述
replaceStaleEntry的背景是基于set的调用
在这里插入图片描述

rehash(全局清理)

在这里插入图片描述

学习资料链接:

视频和文档资料1:

通过网盘分享的文件:视频-由浅入深,全面解析ThreadLocal
链接: https://pan.baidu.com/s/1_CtLTXjBlQ1LlqySTmWpwg?pwd=atfs 提取码: atfs
–来自百度网盘超级会员v8的分享

通过网盘分享的文件:资料-ThreadLocal.zip
链接: https://pan.baidu.com/s/15Vz0WJymEtahX8FL1WNqnQ?pwd=pedv 提取码: pedv
–来自百度网盘超级会员v8的分享

【黑马程序员Java基础教程由浅入深全面解析threadlocal】 https://www.bilibili.com/video/BV1N741127FH/?p=12&share_source=copy_web&vd_source=afbacdc02063c57e7a2ef256a4db9d2a

【黑马Java面试八股文教程,大厂面试必会100题之Java并发之ThreadLocal】 https://www.bilibili.com/video/BV1gs4y137dL/?p=2&share_source=copy_web&vd_source=afbacdc02063c57e7a2ef256a4db9d2a

视频和文档资料2:

【透彻理解ThreadLocal,最新最全的面试题及解答】 https://www.bilibili.com/video/BV1L89wYzEc2/?share_source=copy_web&vd_source=afbacdc02063c57e7a2ef256a4db9d2a

通过网盘分享的文件:带你透彻理解Threadlocal.pdf
链接: https://pan.baidu.com/s/1jNfmpwChXb_qtZqk_F7ybg?pwd=xwyp 提取码: xwyp
–来自百度网盘超级会员v8的分享