• ThreadLocal源码解析 2.ThreadLocalMap内核


    ThreadLocal源码解析—ThreadLocalMap内核

    简介

    内部类 ThreadLocalMap 才是 ThreadLocal 的真正核心。

    ThreadLocalMap 与 HashMap不一样,HashMap 中的数据结构有数组,链表还有红黑树;而 ThreadLocalMap 中的数据结构只有数组。HashMap 处理哈希冲突时会采用链式地址法(拉链法),也就是形成链表;ThreadLocalMap 则是使用开放地址法(线性探测法),如果在存放数据时在一个桶位上发生了冲突,则会向该桶位的后面探测是否有空位,如果到了桶位的尽头也没有空位,就会从哈希表的起始位置继续探测,如果发现还是没有空位,才会进行扩容相关的操作。

    源码解析

    ThreadLocalMap 内部结构

    	static class ThreadLocalMap {
    
         	/*
         	 * 这里的Entry继承了弱引用(弱引用只要发生GC就要被回收)
         	 * Entry的key(ThreadLocal)以弱引用的方式指向ThreadLocal对象。(可以避免内存泄露)
         	 * 什么是弱引用呢?
             * A a = new A();     //强引用
             * WeakReference weakA = new WeakReference(a);  //弱引用
             *
             * a = null;
             * 下一次GC 时 对象a就被回收了,别管有没有 弱引用 是否在关联这个对象。
             *
             * key 使用的是弱引用保留,key保存的是threadLocal对象。
             * value 使用的是强引用,value保存的是 threadLocal对象与当前线程相关联的 value。
             *
             * Entry#key 这样设计有什么好处呢?
             * 当threadLocal对象失去强引用且对象GC回收后,散列表中的与 threadLocal对象相关联的 Entry#key 再次去key.get() 时,拿到的是null。
             * 站在map角度就可以区分出哪些Entry是过期的,哪些Entry是非过期的。
         	 */
            static class Entry extends WeakReference<ThreadLocal<?>> {
                Object value;			
                
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }
    		
         	// 初始化当前map内部 散列表Entry数组的初始长度 16,数组的长度必然是2的次方数
            private static final int INITIAL_CAPACITY = 16;
    		
         	// threadLocalMap 内部散列表数组引用
            private Entry[] table;
    		
         	// Entry数组中的元素个数
            private int size = 0;
     
         	/*
         	 * 扩容阈值 初始值为 len * 2/3
         	 * 触发后调用 rehash()方法
         	 * rehash() 方法先做一次全局检查过期数据,把散列表中所有过期的Entry移除
         	 * 如果移除之后 当前散列表中的Entry个数仍然达到 threshold - threshold/4 => 阈值的3/4,就进行扩容。
         	 */
            private int threshold; 
         
         	// 设置阈值为当前数组长度的2/3
         	private void setThreshold(int len) {
                threshold = len * 2 / 3;
            }
        
         	/*
         	 * 返回当前位置的下一个位置
         	 * @param i 当前下标
         	 * @param len 数组长度
         	 */
            private static int nextIndex(int i, int len) {
                /*
                 * 当前下标+1 如果小于数组长度的话 返回+1后的值
                 * 如果等于数组长度的话,就返回0,从头开始
                 * 实际形成一个环绕式的访问。
                 */
                return ((i + 1 < len) ? i + 1 : 0);
            }
    		
            /*
             * 返回当前位置的上一个位置
             * 跟nextIndex()方法类似。
             */
            private static int prevIndex(int i, int len) {
                /*
                 * 当前下标-1 如果大于等于0 返回-1后的值
                 * 如果当前下标-1 == -1,则说明当前下标为起始位置0,此时 返回散列表最大下标。
                 * 实际形成一个环绕式的访问。
                 */
                return ((i - 1 >= 0) ? i - 1 : len - 1);
            }
    
         	/*
         	 * 构造方法
         	 * 因为Thread.threadLocals字段是延迟初始化的,只有线程第一次存储 threadLocal-value 时 才会创建 threadLocalMap对象。
             * firstKey:threadLocal对象
             * firstValue:当前线程与threadLocal对象相关联的value。
         	 */
            ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
               // 创建一个初始长度为16的Entry数组
               table = new Entry[INITIAL_CAPACITY];
               // 寻址算法:key的hash值 & table.length - 1
               // table数组的长度一定是 2 的次方数。
               // 2的次方数-1 有什么特征呢? 转化为2进制后都是1,16 => 1 0000 - 1 => 1111
               // 1111 与任何数值进行&运算后 得到的数值 一定是 <= 1111
               // i 计算出来的结果 一定是 <= B1111,也就是下标值一定是合法的。
               int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
               // 创建一个Entry对象放到指定的位置上
               table[i] = new Entry(firstKey, firstValue);
               // 数组中存放了1个元素,size设置为1
               size = 1;
               // 设置阈值为初始化容量(16)的2/3 = 10
               setThreshold(INITIAL_CAPACITY);
           }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100

    再谈内部类 Entry

    ThreadLocalMap 的内部类 Entry 继承了 WeakReference(弱引用),在构造方法中调用了父类的构造器。

    从构造器中可以看出 Entry 中的 key 是弱引用类型的,而 value 是强引用类型,也就是一旦发生了 gc,弱引用的对象就会被销毁。

    		static class Entry extends WeakReference<ThreadLocal<?>> {
                Object value;			
                
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
    		}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    为什么要将 ThreadLocalMap 的 key 设置为弱引用呢?为什么不设置为强引用呢?

    这是因为外界是通过 ThreadLocal 来对 ThreadLocalMap 进行操作的,假设外界使用 ThreadLocal 的对象被置 null 了,也就表示不想再使用这个 ThreadLocal 对象了,那 ThreadLocalMap 中的 key 再设置为强引用也没什么用了,反而浪费内存,不如设置成弱引用,gc 时就直接回收掉。可以一定程度上避免内存泄漏问题。

    补充:

    内存泄漏是指程序在申请内存后,无法释放已申请的内存空间。而内存溢出是指程序申请内存时,没有足够的内存供申请者使用。

    可以看这篇文章来复习 java 引用相关知识:Java中的引用: 强引用,软引用,弱引用,虚引用,终结器引用

    image-20221127144505092

    当前方法弹栈后,ABC 的 3 个强引用指针都被置为 null,此时只有 Cache 对象的 3 个弱引用指针指向他们,此时发生 GC,对象 ABC 都会被回收。

    ThreadLocalMap.getEntry()

    // ------------ThreadLocalMap.get()-------------
    // 此方法详解在 [https://blog.csdn.net/qq_46312987/article/details/121799343]
    // 这里我们详细讲解 getEntry()方法。
    	public T get() {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                // 这里调用了getEntry()方法,去threadLocalMap中查询指定的数据
                // this就是当前ThreadLocal对象 作为key传入此方法
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;
                    return result;
                }
            }
            return setInitialValue();
        }
    						||
    		 				||
             				\/ 
    
    // ------------ThreadLocalMap.getEntry()-------------
    		/*
        	 * ThreadLocal对象的get()操作实际上是由ThreadLocalMap.getEntry()完成的。
        	 * @param key:当前ThreadLocal对象
        	 */
    		private Entry getEntry(ThreadLocal<?> key) {
                // 寻址 获取下标
                int i = key.threadLocalHashCode & (table.length - 1);
                // 获取指定位置的Entry
                Entry e = table[i];
                /*
                 * 当前位置不为null,并且判断当前位置的key(ThreadLocal对象)与传来的key是否一致
                 * 注意:ThreadLocalMap与HashMap或者是ConcurrentHashMap最大的区别就是:
                 * 当出现hash冲突时使用的是开放寻址法(找下一个位置)而不是拉链法(在当前位置形成链表)
                 */
                if (e != null && e.get() == key)
                    return e; // 找到了则直接返回
                else
                    // 在当前位置没找到就去后面继续找
                    return getEntryAfterMiss(key, i, e);
            }	
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    ThreadLocalMap.getEntryAfterMiss()

    		/*
    		 * 此方法的作用就是从当前位置向后查询,查询到指定数据返回,当查询到某一个位置的Entry为null时结束,最终返回null,
    		 * 在查询过程中如果某一个位置的Entry不为null,但是key为null,
    		 * 说明对应的当前Entry关联的ThreadLcoal对象已经被回收了,那么就会将当前的Entry清理掉。
    		 * @param key   ThreadLocal对象
    		 * @param i     第一次寻址的索引位置
    		 * @param e     table第i个位置的Entry对象
             */		
            private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
                Entry[] tab = table;
                int len = tab.length;
                // 循环向后查找
                // 条件:e != null 说明 向后查找的范围是有限的,碰到 slot == null 的情况,搜索结束。
                while (e != null) {
                    // Entry继承了弱引用,get()方法就是获取内部的ThreadLocal对象
                    ThreadLocal<?> k = e.get();
                    // 查找成功,返回Entry
                    if (k == key)
                        return e;
                    /* 
                     * key为null,说明Entry关联的ThreadLocal被GC回收了..(因为key是弱引用 key = e.get() == null)
                     * 但是Entry还存在,这时就需要将当前位置的Entry干掉。
                     */
                    if (k == null)
                        // 进行一次 探测式的过期数据回收
                        expungeStaleEntry(i);
                    // k不为null,但是当前Entry不是目标Entry,继续向后查找
                    else
                        // 更新index
                        i = nextIndex(i, len);
                    // 获取下一个slot中的Entry
                    e = tab[i];
                }
                // 执行到这里,说明关联区段内都没找到相应数据,返回null
                return null;
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    ThreadLocalMap.expungeStaleEntry()

    • 探测式清理
    		/*
    		 * 探测式清理:探测式的过期数据回收,清理区间内的每一个过期数据
    		 * 1.即当前位置上的Entry的key是null,说明当前的Entry已经没有用了,需要将其干掉。
    		 * 2.遍历哈希表,将从当前位置开始的Entry != null && key == null的所有Entry干掉,然后将正常的Entry做一次重新迁移,优化查询。
    		 * @param staleSlot (stale翻译为过期的)
    		 * table[staleSlot] 就是一个过期数据,以这个位置开始 继续向后查找过期数据,直到碰到 slot == null 的情况结束。
    		 */
            private int expungeStaleEntry(int staleSlot) {
                Entry[] tab = table;
                int len = tab.length;
                // 将当前位置的Entry的的value置为null(help GC)
                tab[staleSlot].value = null;
                // 将当前位置置为null,将Entry直接干掉
                tab[staleSlot] = null;
                // 干掉了一个元素,所以size - 1
                size--;
    			
                /*
                 * 下面就是rehash()的过程(重新计算有效元素的索引,并清理过期元素)
                 */
                
                // 当前遍历的Entry
                Entry e;      
                // 当前的位置
                int i;
     			// 循环遍历数组,从置为null的Entry对象的下一个位置(staleSlot + 1)开始,直到某一位置上的Entry为null为止。
                for (i = nextIndex(staleSlot, len);
                     (e = tab[i]) != null;
                     i = nextIndex(i, len)) { 	
                    /*
                     * 能进入for循环,说明当前位置的Entry一定不为null
                     */
                    // 获取当前节点的key
                    ThreadLocal<?> k = e.get(); 
                  	// k(ThreadLocal对象) = null,说明k表示的threadLocal对象 已经被GC回收了..当前entry属于脏数据了(过期)..
                    // 将当前Entry的value和当前Entry全部干掉。
                    if (k == null) {
                        e.value = null;
                        tab[i] = null;
                        size --;
                    } else {
                        /*
                         * 执行到这里,说明当前遍历的slot中对应的entry 是非过期数据
                         * 此时需要做的事就是判断当前位置上的Entry是否在经过哈希寻址后应该在的位置,(因为有可能发生过冲突),往后偏移存储了, 
                         * 如果不在该在的位置,这个时候应该去优化位置,就去寻找距离寻址位置最近的位置(也可能找到寻址的位置)。 
                         * 这样的话,查询的时候 效率才会更高!
                         */     
                        // 重新计算索引
                        int h = k.threadLocalHashCode & (len - 1);
                        // 条件成立:表示当前Entry确实不在该在的位置,就是发生过hash冲突,向后偏移过了,需要尝试重新找位置存放。
                        if (h != i) {
                            // 先将当前位置置为null
                            tab[i] = null;
                            // 循环找位置存放
                            // 以正确位置h 开始,向后查找第一个 可以存放Entry的位置。
                            while (tab[h] != null)
                                h = nextIndex(h, len);
                            // 将当前元素放入到 距离正确位置 更近的位置(有可能就是正确位置)
                            tab[h] = e;
                        }
                    }
                }
                return i; // 返回当前下标
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    expungeStaleEntry() 方法流程图

    • 红色表示过期数据,灰色/白色表示对应 slot 为 null,绿色表示正常状态(slot 中有数据)。

    image-20221108155811495

    ThreadLocal.get() 方法流程图

    • 这张图是以 ThreadLocal.get() 层面分析的,并未画出 ThreadLocal.getEntry() 内部方法的逻辑。

    image-20221108162025344

    ThreadLocalMap.set()

    // -----------------ThreadLocal.set()----------------------
    	public void set(T value) {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                // ThreadLocalMap.set()方法,真正给当前线程添加 threadLocal-value 键值对 或 重写value的逻辑。
                map.set(this, value);
            else
                createMap(t, value);
        }
    				 ||
    		 		 ||
             		 \/ 
                                
    // -----------------ThreadLocalMap.set()----------------------
    	private void set(ThreadLocal<?> key, Object value) {       
                // 寻址
                Entry[] tab = table;
                int len = tab.length;
                int i = key.threadLocalHashCode & (len-1);
                         
                // 什么slot可以使用呢??
                // 1.k == key 说明是替换
                // 2.碰到一个过期的slot,这个时候 咱们可以强行占用
                // 3.查找过程中 碰到 slot == null 了
    
                /*
                 * for循环做的事就是,循环寻找key相同的Entry
                 * 1.找到相同key并且正常的Entry,做value替换
                 * 2.找到某一位置(Entry != null && Entry.key == null),将Entry替换
                 */
                for (Entry e = tab[i];
                     e != null;
                     e = tab[i = nextIndex(i, len)]) { // 依次往后遍历
                    ThreadLocal<?> k = e.get();
                    // 找到了相同的key,替换value
                    if (k == key) {
                        e.value = value;
                        return;
                    }		
                    // 查找过程中,碰到了Entry.key == null,说明当前Entry是过期数据
                    if (k == null) {
                        // 替换过期的Entry
                        replaceStaleEntry(key, value, i);
                        return;
                    }
                }	
                /*
                 * 执行到这里,说明for循环找到了一个当前slot为null的情况
                 * 此时直接在这个slot位置上创建一个Entry对象。
                 */
                tab[i] = new Entry(key, value);
                int sz = ++size; 
                /*
                 * 这里做一次启发式清理工作
                 * 条件1:cleanSomeSlots()返回false表示内部没有清理到数据 取反后为true 进入条件2逻辑
                 * 条件2:这时在判断元素数量是否达到了扩容阈值,大于等于阈值就进行rehash()操作
                 */
                if (!cleanSomeSlots(i, sz) && sz >= threshold)
                    rehash();
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    ThreadLocalMap.replaceStaleEntry()

    		/*
    		 * 替换过期的Entry
    		 * @param key 新key
    		 * @param value 新value
    		 * @param staleSlot 过期Entry的位置
    		 */
             private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                           int staleSlot) {
            	Entry[] tab = table;
                int len = tab.length;
                Entry e;
                // 将过期Entry的位置赋值给slotToExpunge,表示开始探测式清理过期数据的 开始下标(默认从当前 staleSlot开始)
                int slotToExpunge = staleSlot;
                /*
                 * 以当前staleSlot位置的前一个位置开始,向前迭代查找,
                 * (for循环结束条件 Entry = null),更新slotToExpunge为靠前的(Entry != null && Entry.key == null)的过期Entry的位置。
                 */
                for (int i = prevIndex(staleSlot, len);
                     (e = tab[i]) != null;
                     i = prevIndex(i, len))
                    // 条件成立:说明向前找到了过期数据,更新探测清理过期数据的 开始下标为 i
                    if (e.get() == null)
                        slotToExpunge = i;
                /*
                 * 以当前不合法的Entry的位置(staleSlot)的下一个位置向后去查找,直到碰到null为止。
                 */
                for (int i = nextIndex(staleSlot, len);
                     (e = tab[i]) != null;
                     i = nextIndex(i, len)) {  
                    // 获取当前位置的Entry的key(threadLocal)
                    ThreadLocal<?> k = e.get();
                    /*
                     * key要添加的新key
                     * k 当前遍历的key
                     * k == key 说明要添加的key已经存在了,需要替换value
                     * 然后做清理逻辑
                     */
                    if (k == key) {
                        // 替换为新value
                        e.value = value;
                        // 交换位置的逻辑
                        // 将过期的Entry放到当前位置i,因为下面要从i这个位置开始清理
                        tab[i] = tab[staleSlot];
                        // 将替换完毕的Entry放到过期数据的位置,这样的话,咱们这个数据位置就被优化了..
                        tab[staleSlot] = e;
                        // 条件成立:
                        // 1.说明replaceStaleEntry一开始时向前查找过期数据时,并未找到过期的Entry
                        // 2.向后检查过程中也未发现过期数据..
                        if (slotToExpunge == staleSlot)
                        	// 因为上面做了交换,所以当前位置i就是过期数据,赋值给slotToExpunge
                            slotToExpunge = i;
                        // 下面就是清理过期的Entry
                        // 启发式清理
                        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                        return;
                    }
                    /* 
                     * 条件1:k == null成立,说明当前向后遍历到的Entry是一个过期数据
                     * 条件2:slotToExpunge == staleSlot成立说明一开始向前查找过期数据并未找到过期的Entry
                     */
                    if (k == null && slotToExpunge == staleSlot)
                        // 因为向后查询过程中查找到了一个过期数据,更新slotToExpunge为当前位置
                        // 前提条件是前驱扫描时未发现过期数据
                        slotToExpunge = i;
                }
         		/*
         		 * 什么时候执行到这里?
         		 * —>向后查找过程中,并未发现 key = null 的Entry,说明当前set操作是一个添加逻辑,直接将新数据添加到过期Entry的位置上。
         		 */
                tab[staleSlot].value = null;
                tab[staleSlot] = new Entry(key, value);
         		// 条件成立:说明除了当前staleSlot过期Entry位置以外,还发现其它的过期slot了
                if (slotToExpunge != staleSlot)
                    // 开启清理数据的逻辑
                    // 启发式清理
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77

    replaceStaleEntry() 方法流程图

    image-20221127144707196

    ThreadLocalMap.cleanSomeSlots()

    • 启发式清理
           /*
            * 启发式清理:试探的扫描一些单元格,寻找过期元素并清理(并不是扫描所有)。
            * 启发式清理作用:指导我们找到脏Entry,并且通过 探索式清理流程 清理该脏Entry(真正的清理逻辑还是expungeStaleEntry()方法)
            * @param i 表示清理工作的起始位置(探测式清理结束的位置),这个位置一定是null
            * @param n 表示table.length 这里也表示结束条件
            * @return removed:true->表示清理过数据 false->表示未清理过数据
            */
    		private boolean cleanSomeSlots(int i, int n) {
                // 表示启发式清理工作 是否清除过过期数据
                boolean removed = false;
                Entry[] tab = table;
                int len = tab.length;
                do {
                    // 获取当前i位置的下一个位置
                    // 这里为什么不是从i就检查呢?
                    // 因为cleanSomeSlots(i = expungeStaleEntry(???), n),expungeStaleEntry(???)的返回时该位置的slot一定是null。
                    i = nextIndex(i, len);
                    // 获取下标i对应的Entry
                    Entry e = tab[i];
                    /*
                     * 条件成立:表示当前位置的Entry是过期数据,需要清理
                     */
                    if (e != null && e.get() == null) {
                        // 重新更新n为 table数组长度
                        n = len;
                        // 清理标志置为true,表示清理过数据
                        removed = true;
                        // 以当前过期的slot位置开始,做一次探测式清理工作。
                        i = expungeStaleEntry(i);
                    }
                /*
                 * while循环条件中的 (n >>>= 1) != 0) 表示循环次数
                 * 在扫描过程中,
    			 * 如果没有遇到脏entry就整个扫描过程持续log2(n)次,log2(n)的得来是因为n >>>= 1,每次n右移一位相当于n除以2。
    			 * 如果在扫描过程中遇到脏entry的话就会令n为当前hash表的长度(n=len),再扫描log2(n)趟,注意此时n增加无非就是多增加了循环次数,
    			 * 让有更多的循环次数支持我们在后面的循环里面有机会能够找到脏entry,并且调用expungeStaleEntry()方法做探测式清理。
                 */
                // 假设table长度为16
                // 16 >>> 1 => 8
                // 8  >>> 1 => 4
                // 4  >>> 1 => 2
                // 2  >>> 1 => 1
                // 1  >>> 1 => 0
                } while ( (n >>>= 1) != 0);
                return removed;
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    cleanSomeSlots() 方法流程图

    image-20221127144755582

    ThreadLocalMap.set() 方法流程图

    流程总结

    • 先遍历 hash 表查找元素
      • 找到相同的 key 就做 value 的替换操作
      • 未找到,可能发生 hash 冲突,继续向后寻找
        • 找到了过期 Entry,进行清理和替换操作
      • 遇到了空位,说明是第一次添加元素,新建一个 Entry 对象

    在这里插入图片描述

    ThreadLocalMap.rehash()

    • 只有当前 table 元素个数大于等于扩容阈值并且在清理完 table 内部的所有过期的 Entry 后,元素个数还大于等于阈值的3/4,这时才会触发扩容。
    		// ThreadLocalMap.set()方法会调用rehash()方法
    		// rehash()方法只是先调用了一次expungeStaleEntries()方法进行清理工作,然后再判断是否达到扩容条件,再调用真正的resize()扩容方法、
    		// rehash()只是个表层方法。
    		private void rehash() {
                // 这个方法执行完毕后,当前散列表内部所有过期的数据,都会被干掉。
                // 遍历每个桶位 进行探测式清理(全面清理)
                expungeStaleEntries();
                
                // 条件成立:说明清理完所有的过期Entry后,size数量仍然达到了扩容阈值的 3/4,才会真正触发扩容!
                if (size >= threshold - threshold / 4)
                    // 做一次resize()扩容
                    resize();
            }
    
    // ----------------ThreadLocalMap.expungeStaleEntries()----------------
    		// 这个方法执行完毕后,当前散列表内部所有过期的数据,都会被干掉。
    		private void expungeStaleEntries() {
                Entry[] tab = table;
                int len = tab.length;
                for (int j = 0; j < len; j++) {
                    Entry e = tab[j];
                    // 遍历每个桶位 进行探测式清理(全面清理)
                    if (e != null && e.get() == null)
                        expungeStaleEntry(j);
                }
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    ThreadLocalMap.resize()

    		/*
             * 扩容操作
             */
            private void resize() {  
                // 扩容,变为原长度的2倍
                Entry[] oldTab = table;
                int oldLen = oldTab.length;
                int newLen = oldLen * 2; // 新长度 = 原长度 * 2
                // 创建一个新的table
                Entry[] newTab = new Entry[newLen];
                // 表示新表中的元素个数
                int count = 0;
                // 遍历原表中的每一个slot,将原表中的数据迁移到新表
                for (int j = 0; j < oldLen; ++j) {
                    Entry e = oldTab[j];
                    if (e != null) {
                        ThreadLocal<?> k = e.get();
                        // 条件成立:说明当前位置的Entry是过期数据
                        if (k == null) {
                            e.value = null; // Help the GC
                        } else {
                            // 执行到这里 说明原表当前位置的Entry是正常数据 需要迁移到新表
                            // 计算新位置
                            int h = k.threadLocalHashCode & (newLen - 1);
                            // 遍历找空位置(找到距离目标位置最近的一个slot)
                            while (newTab[h] != null)
                                h = nextIndex(h, newLen);
                            // 放到新位置
                            newTab[h] = e;
                            // 新表元素个数+1
                            count++;
                        }
                    }
                }
                setThreshold(newLen); // 设置新的扩容阈值
                size = count; 		  // 将count赋值给size
                table = newTab; 	  // 将新表赋值给table
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    ThreadLcoalMap.remove()

    // --------------------ThreadLocal.remove()-------------------------
    	public void remove() {
            ThreadLocalMap m = getMap(Thread.currentThread());
            if (m != null)
                // ThreadLocalMap.remove()方法 真正删除的逻辑
                m.remove(this);
    	}
    
    // --------------------ThreadLcoalMap.remove()-------------------------
    	/*
         * 真正的remove逻辑:先将对应key => threadLocal置为null,然后将Entry内部的value以及Entry干掉。
         */ 
        private void remove(ThreadLocal<?> key) {
                Entry[] tab = table;
                int len = tab.length;
            	// 根据key获取索引位置
                int i = key.threadLocalHashCode & (len - 1);
                // 遍历
                for (Entry e = tab[i];
                     e != null;
                     e = tab[i = nextIndex(i, len)]) {
                    // 找到指定key
                    if (e.get() == key) {                
                        /*
                         * Entry是弱引用,调用clear()方法会将内部关联的threadLocal置为null
                         */
                        e.clear();
                        // 以当前位置 进行一次探索式清理 将Entry内部的value以及Entry干掉
                        expungeStaleEntry(i);
                        return;
                    }
                }
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    ThreadLocal 内存泄露问题

    看了上文的讲解,我们知道 ThreadLocalMap 内部维护了一个 Entry 数组,而内存泄漏与这个 Entry 数组有很大的关系。

    Entry 对象的 key 是 ThreadLocal 对象,是一个弱引用,而 value 是强引用。如果外部的 ThreadLocal 被置为 null,也就是 key 的强引用消失了, 此时只有一个弱引用还在,而这时又发生了 GC,key 就被回收掉了,此时 Entry 对象还在强引用 value,导致 value 无法被回收,我们也无法获取到这个 value,此时就造成了「内存泄漏」,value 成了一个永远也无法被访问,但是又无法被回收的对象。

    但是要注意一个误区,ThreadLocal 造成内存泄漏的可能性很低,而且不是将 ThreadLocalMap 中 Entry 的 key 设计为弱引用容易导致内存泄漏问题,相反,将 key 设置为弱引用反而防止了内存泄漏的发生。

    Entry 的 key 设计为弱引用是 JDK 在尽量避免程序出现内存泄漏,通过上文的分析可以看出 ThreadLocal 做了保护措施,在操作 ThreadLocal 时,如果发现 key 为 null 也就代表这些节点已经是过期节点,就会将其自发的清理掉。

    所以,如果在线程池(线程复用)环境下,如果还会调用ThreadLocal的 set/get/remove 方法,就不会发生长期的内存泄漏问题。

    同时我们想想,如果 key 设计成强引用而不是弱引用会怎样?

    如果 key 设计为强引用,外部的 ThreadLocal 引用被置为 null 了,此时 Entry 中的 key 所引用 ThreadLocal 对象就没有了存在的意义,无法获取到,还没法被回收,造成了内存泄漏。

    ThreadLocal 会在以下过程中清理过期节点:

    1. 调用 set() 方法时,采样清理、全量清理,扩容时还会继续检查。
    2. 调用 get() 方法,没有直接命中,向后环形查找时。
    3. 调用 remove() 时,除了清理当前 Entry,还会向后继续清理。
    • 在 remove、set、get 的时候都会去主动寻找过期的数据,不是将其干掉,就是将其替换掉。
    • 但是如果存储的是一个很大的对象,就会有问题,在线程执行完毕之后,没有及时释放,那么就会造成 ThreadLocalMap 内的垃圾数据没有被及时清除。
    • 这个实时候就会造成内存泄露问题。
    • 所以在使用的时候,一定要在 finally 方法中将其 remove 掉。
    • 比如实际开发的时候,线程池中的线程有可能一直被复用,没有被回收掉,那么这个时候就可能存在内存泄露问题。
    • 调用 get、set 或 remove 方法时,就会尝试删除 key 为 null 的 Entry,可以释放 value 对象所占用的内存。
      • 当我们为 threadLocal 变量赋值,实际上就是当前的 Entry(threadLocal实例为key,值为value),往这个 threadLocalMap 中存放。Entry 中的 key 是弱引用,当 threadLocal 外部强引用被置为 (tl=null),那么系统 GC 的时候,根据可达性分析,这个 threadLocal 实例就没有任何一条链路能够引用到它,这个 ThreadLocal 势必会被回收,这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap-> Entry -> value 永远无法回收,造成内存泄漏。
      • 当然,如果当前 thread 运行结束,threadLocal,threadLocalMap,Entry 没有引用链可达,在垃圾回收的时候都会被系统进行回收。
      • 但在实际使用中我们有时候会用线程池去维护我们的线程,比如在 Executors.newFixedThreadPool() 时创建线程的时候,为了复用线程是不会结束的,所以 threadLocal 内存泄漏就值得我们小心。

    ThreadLocal 最佳实践

    虽然 ThreadLocal 有避免内存泄漏发生的机制,但并不是万无一失的,所以在我们使用时也要注意一些。

    • 一般建议将其声明为 static final 或者 static 的,避免频繁创建 ThreadLocal 实例。
    • 每次使用完 ThreadLocal,都尽量调用它的 remove() 方法,清理数据。
    • 尽量避免存储大对象,如果非要存,那么尽量在访问完成后及时调用 remove() 删除掉。

    ThreadLocal 应用场景

    • 线程不安全 DateUtils

      public class UnSafeDateUtils {
          private static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
          public static void main(String[] args) {
              for (int i = 0; i < 100; i++) {
                  new Thread(() -> {
                      try {
                          Thread.sleep(10);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      String res = format.format(new Date());
                      System.out.println(res);
                      try {
                          System.out.println(format.parse(res));
                      } catch (ParseException e) {
                          e.printStackTrace();
                      }
                  }).start();
              }
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21

      多线程下会抛出各种奇怪的异常

      Exception in thread "Thread-88" java.lang.NumberFormatException: For input string: ""
      	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
      	at java.base/java.lang.Long.parseLong(Long.java:702)
      	at java.base/java.lang.Long.parseLong(Long.java:817)
      	at java.base/java.text.DigitList.getLong(DigitList.java:195)
      	at java.base/java.text.DecimalFormat.parse(DecimalFormat.java:2093)
      	at java.base/java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2222)
      	at java.base/java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1529)
      	at java.base/java.text.DateFormat.parse(DateFormat.java:386)
      	at lyc.Main.lambda$main$0(Main.java:21)
      	at java.base/java.lang.Thread.run(Thread.java:844)
      Exception in thread "Thread-29" java.lang.NumberFormatException: multiple points
      	at java.base/jdk.internal.math.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1914)
      	at java.base/jdk.internal.math.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
      	at java.base/java.lang.Double.parseDouble(Double.java:543)
      	at java.base/java.text.DigitList.getDouble(DigitList.java:169)
      	at java.base/java.text.DecimalFormat.parse(DecimalFormat.java:2098)
      	at java.base/java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1915)
      	at java.base/java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1529)
      	at java.base/java.text.DateFormat.parse(DateFormat.java:386)
      	at lyc.Main.lambda$main$0(Main.java:21)
      	at java.base/java.lang.Thread.run(Thread.java:844)
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
    • 使用 ThreadLocal 解决

      import org.apache.commons.lang3.StringUtils;
      
      import java.text.ParseException;
      import java.text.SimpleDateFormat;
      import java.util.Date;
      import java.util.HashMap;
      import java.util.Map;
      
      public class DateUtils {
          /** YYYY */
          public final static String YYYY = "yyyy";
          /** MM_DD */
          public final static String MM_DD = "MM-dd";
          /** HH_MM_SS */
          public final static String HH_MM = "HH:mm";
          /** HH_MM_SS */
          public final static String HH_MM_SS = "HH:mm:ss";
          /** YYYY_MM */
          public static final String YYYY_MM = "yyyy-MM";
          /** YYYY_MM_DD */
          public static final String YYYY_MM_DD = "yyyy-MM-dd";
          /** YYYY_MM_DD_HH */
          public final static String YYYY_MM_DD_HH = "yyyy-MM-dd HH";
          /** YYYY_MM_DD_HH_MM */
          public final static String YYYY_MM_DD_HH_MM = "yyyy-MM-dd HH:mm";
          /** YYYY_MM_DD_HH_MM_SS */
          public final static String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
          /** 本地线程日期格式 */
          private final static ThreadLocal<Map<String, SimpleDateFormat>> THREAD_LOCAL_FORMATTERS = ThreadLocal.withInitial(() -> {
              Map<String, SimpleDateFormat> map = new HashMap<>(16);
              map.put(YYYY, new SimpleDateFormat(YYYY));
              map.put(YYYY_MM, new SimpleDateFormat(YYYY_MM));
              map.put(MM_DD, new SimpleDateFormat(MM_DD));
              map.put(HH_MM, new SimpleDateFormat(HH_MM));
              map.put(HH_MM_SS, new SimpleDateFormat(HH_MM_SS));
              map.put(YYYY_MM_DD, new SimpleDateFormat(YYYY_MM_DD));
              map.put(YYYY_MM_DD_HH, new SimpleDateFormat(YYYY_MM_DD_HH));
              map.put(YYYY_MM_DD_HH_MM, new SimpleDateFormat(YYYY_MM_DD_HH_MM));
              map.put(YYYY_MM_DD_HH_MM_SS, new SimpleDateFormat(YYYY_MM_DD_HH_MM_SS));
              return map;
          });
          /**
           * YYYY 转换日期
           *
           * @param dateString the time
           * @param format format
           * @return date date
           */
          public static Date parse(String dateString, String format) {
              if (StringUtils.isBlank(dateString)) {
                  return null;
              }
              try {
                  return getFormat(format).parse(dateString);
              } catch (ParseException e) {
                  return null;
              }
          }
          /**
           * 格式化日期
           *
           * @param date the time
           * @param format format
           * @return date date
           */
          public static String format(Date date, String format) {
              if (StringUtils.isBlank(format)) {
                  return null;
              }
              try {
                  return getFormat(format).format(date);
              } catch (Exception e) {
                  return null;
              }
          }
          /**
           * 获取格式化配置
           *
           * @param format 格式化
           * @return 配置
           */
          private static SimpleDateFormat getFormat(String format) {
              return THREAD_LOCAL_FORMATTERS.get().get(format);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85

    更多 ThreadLocal 应用场景可参考:史上最全ThreadLocal 详解(一)


    参考

  • 相关阅读:
    Java 同步锁ReentrantLock与抽象同步队列AQS
    【数据结构初阶】三、 线性表里的链表(无头+单向+非循环链表)
    OPENCV进行图像修复
    缺口的大利润!伦敦银如何使用缺口交易
    【机器学习】Tensorflow.js:我在浏览器中使用机器学习实现了图像分类
    final关键字
    【vue网站优化】秒开网页
    JAVAWeb--会话_过滤器_监听器
    二十四、W5100S/W5500+RP2040树莓派Pico<PHY的状态模式控制>
    [激光原理与应用-29]:典型激光器 -1- 固体激光器
  • 原文地址:https://blog.csdn.net/weixin_53407527/article/details/128064244