ThreadLocal 详解

1.ThreadLocal是什么? / 为什么要使用ThreadLocal?

ThreadLocal是什么?

  ThreadLocal就是一个java类,这个类的作用和线程局部变量有关。线程局部变量作用域是当前单个线程,在线程开始时分配,线程结束时回收

  ThreadLocal类位于java.lang包下,由JDK包提供。如果创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,多个线程操作这个变量的时候,其实是在操作自己本地内存里的变量,他们可以使用 get()set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题

为什么要使用ThreadLocal?

  并发场景下,会存在多个线程同时修改一个共享变量的场景,这就可能会出现线程安全问题。为了解决线程安全问题,可以用加锁的方式,比如synchronized或者lock,但是加锁可能会导致系统变慢

 

2.ThreadLocal的使用

复制代码
public class UserContext {
    private static ThreadLocal threadLocal = new ThreadLocal();

    public static User getUser() {
        return (User) threadLocal.get();
    }

    public static void setUser(User user) {
        threadLocal.set(user);
    }

    public static void remove() {
        threadLocal.remove();
    }
}
复制代码

 

6.GC 之后 key 是否为 null?

  ThreadLocalkey是弱引用,那么在ThreadLocal.get()的时候,发生GC之后,key是否是null

  ThreadLocalMap 的 set 方法,通过调用 replaceStaleEntry 方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏。

你把key这个引用赋值为null, 并不意味着Hashmap里的key也是null.

 

7.ThreadLocalMap的Hash算法

  最关键的就是threadLocalHashCode值的计算,ThreadLocal中有一个属性为HASH_INCREMENT = 0x61c88647。

  每创建一个ThreadLocal对象,这个ThreadLocal.nextHashCode 这个值就会增长 0x61c88647 。这个值很特殊,它是斐波那契数 也叫 黄金分割数hash增量为 这个数字,带来的好处就是 hash 分布非常均匀

复制代码
int i = key.threadLocalHashCode & (len-1);

public class ThreadLocal<T> {
    private final int threadLocalHashCode = nextHashCode();

    private static AtomicInteger nextHashCode = new AtomicInteger();

    private static final int HASH_INCREMENT = 0x61c88647;

    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

    static class ThreadLocalMap {
        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);
        }
    }
}
复制代码

 

8.ThreadLocalMap的Hash冲突

  如果我们插入一个value=27的数据,通过 hash 计算后应该落入槽位 4 中,而槽位 4 已经有了 Entry 数据。此时就会线性向后查找,一直找到 Entrynull 的槽位才会停止查找,将当前元素放入此槽位中。当然迭代过程中还有其他的情况,比如遇到了 Entry 不为 nullkey 值相等的情况,还有 Entry 中的 key 值为 null 的情况等等都会有不同的处理。在set过程中,如果遇到了key过期的Entry数据,实际上是会进行一轮探测式清理操作的。

 

ThreadLocalMap.set()详解:

第一种情况: 通过hash计算后的槽位对应的Entry数据为空:直接将数据放到该槽位即可。

第二种情况: 槽位数据不为空,key值与当前ThreadLocal通过hash计算获取的key值一致:直接更新该槽位的数据。

第三种情况: 槽位数据不为空,往后遍历过程中,在找到Entrynull的槽位之前,没有遇到key过期的Entry:遍历散列数组,线性往后查找,如果找到Entrynull的槽位,则将数据放入该槽位中,或者往后遍历过程中,遇到了key 值相等的数据,直接更新即可。

第四种情况: 槽位数据不为空,往后遍历过程中,在找到Entrynull的槽位之前,遇到key过期的Entry,如下图,往后遍历过程中,遇到了index=7的槽位数据Entrykey=null:表明此数据key值已经被垃圾回收掉了,此时就会执行replaceStaleEntry()方法,该方法含义是替换过期数据的逻辑,以index=7位起点开始遍历,进行探测式数据清理工作。

replaceStaleEntry():

(1)初始化探测式清理过期数据扫描的开始位置:slotToExpunge=staleSlot(过期桶位置)=7

(2)以当前节点向前迭代,检测是否有过期的entry,如果有则更新slotToExpunge值,继续向前迭代,碰到null则结束探测。

(3)从当前staleSlot向后迭代

  查找到key值相等的Entry元素:找到后更新Entry的值并交换staleSlot元素的位置,更新数据。(4)开始清理工作,从slotToExpunge开始向后检查过期数据,并清理。

  未查找到key值相等的Entry元素:直到Entry为null则停止查找。(4)创建新的Entry,替换table[staleSlot]位置,进行过期元素清理工作。

 

清理工作的两个方法:

探测式清理:expungeStaleEntry()

  以当前Entry 往后清理,遇到值为null则结束清理,属于线性探测清理

  遍历散列数组,从开始位置向后探测清理过期数据,将过期数据的Entry设置为null,沿途中碰到未过期的数据则将此数据rehash后重新在table数组中定位,如果定位的位置已经有了数据,则会将未过期的数据放到最靠近此位置的Entry=null的桶中,使rehash后的Entry数据距离正确的桶的位置更近一些。

启发式清理:cleanSomeSlots()

  试探性地扫描一些单元格以查找过时的条目。

  在cleanSomeSlots内部会选择不同的位置为起点,轮询调用expungeStaleEntry()进行向后探测清理。

复制代码
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}

//在新增或删除脏元素的时候,通过对数方式扫描一些脏的Entry,作为不扫描和与节点数量成比例的扫描之间的平衡。
  不扫描:快速但是保留垃圾
  节点数量成比例扫描:需要花费O(n)时间,清空所有垃圾数据
复制代码

 

9.ThreadLocalMap扩容机制:

  在ThreadLocalMap.set()方法的最后,如果执行完启发式清理工作后,未清理到任何数据,且当前散列数组中Entry的数量已经达到了列表的扩容阈值(len*2/3),就开始执行rehash()逻辑:

复制代码
private void rehash() {
    //进行探测式清理工作
    expungeStaleEntries();

    //根据size >= threshold * 3/4来决定是否扩容
    if (size >= threshold - threshold / 4)
        resize();
}

private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {//从table的起始位置往后进行探测式清理
        Entry e = tab[j];
        //如果是过期entry
        if (e != null && e.get() == null)//e.get()为key
            expungeStaleEntry(j);
    }
}
复制代码

!!!注意:entry.size达到threshold=len*2/3 ---> rehash()

    entry.size达到threshold*3/4 ---> resize()

 resize():

  扩容后的tab的大小为oldLen * 2,然后遍历老的散列表,重新计算hash位置,然后放到新的tab数组中,如果出现hash冲突则往后寻找最近的entrynull的槽位,遍历完成之后,oldTab中所有的entry数据都已经放入到新的tab中了。重新计算tab下次扩容的阈值。

 

ThreadLocalMap.get():

java.lang.ThreadLocal.ThreadLocalMap.getEntry()

第一种情况: 通过查找key值计算出散列表中slot位置,然后该slot位置中的Entry.key和查找的key一致,则直接返回。

第二种情况: 通过查找key值计算出散列表中slot位置,该slot位置中的Entry.key和要查找的key不一致,需要继续往后迭代,迭代到过期entry时会触发一次探测式数据回收,执行expungeStaleEntry()方法,执行之后过期entry被回收,正常数据会尽可能存放在离正确位置更近的位置。此时从该位置继续往后迭代,就可能会找到key值相等entry了。

 

  以get(ThreadLocal1)为例,通过hash计算后,正确的slot位置应该是 4,而index=4的槽位已经有了数据,且key值不等于ThreadLocal1,所以需要继续往后迭代查找。

  迭代到index=5的数据时,此时Entry.key=null,触发一次探测式数据回收操作,执行expungeStaleEntry()方法,执行完后,index 5,8的数据都会被回收,而index 6,7的数据都会前移。index 6,7前移之后,继续从 index=5 往后迭代,于是就在 index=5 找到了key值相等的Entry数据。

 

ThreadLocal使用场景

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @   壹索007  阅读(104)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
点击右上角即可分享
微信分享提示