首页 > 其他分享 >深入理解 ThreadLocal

深入理解 ThreadLocal

时间:2024-03-03 17:44:35浏览次数:26  
标签:线程 get ThreadLocal 理解 深入 key Entry null

目录

1. ThreadLocal是什么,它有哪些特性?

ThreadLocal是线程变量,ThreadLocal中设置的变量属于当前线程,该变量对其它线程而言是隔离的。 ThreadLocal在每个线程中都创建了一个变量副本,每个线程可以访问自己内部的副本变量。

ThreadLocal具有以下特性:

  1. 并发性:在多线程并发场景下使用。
  2. 传递数据:可以通过ThreadLocal在同一线程的上下文中传递参数
  3. 线程隔离:每个线程变量都相互独立,不会相互影响。

ThreadLocal相关的核心API:

  • get() 方法用于获取当前线程的副本变量值。
  • set() 方法用于保存当前线程的副本变量值。
  • initialValue() 为当前线程初始副本变量值。
  • remove() 方法移除当前线程的副本变量值。

ThreadLocal几个重要的变量

// 计算hash值
private final int threadLocalHashCode = nextHashCode();
// 使用原子类记录hash值
private static AtomicInteger nextHashCode =
    new AtomicInteger();
// 魔数,更好的分散数据
private static final int HASH_INCREMENT = 0x61c88647;
// 初始化容量
private static final int INITIAL_CAPACITY = 16;
// 存储数据数组
private Entry[] table;
// 数组中的元素个数
private int size = 0;
// 扩容的阈值,默认是数组大小的三分之二
private int threshold;
123456789101112131415

例子1: ThreadLocal在CLHLock自旋锁中的使用:

public class CLHLock implements Lock {

    //队列尾部的QNode,用AtomicReference,自带CAS来非阻塞同步?
    private AtomicReference<QNode> tail;

    //用ThreadLocal来解决跨线程的数据同步性能问题

    //当前线程维护自己的QNode
    private ThreadLocal<QNode> myNode;

    //当前线程的QNode的前驱
    private ThreadLocal<QNode> preNode;

    //重入次数
    //为什么用ThreadLocal呢?每个线程管理自己的count,不用每次读取主内存中读取
    //虽然Atomic通过volatile和CAS保证了线程安全,但是还有从主内存中读写的性能消耗
    private ThreadLocal<AtomicInteger> count;

    public CLHLock() {
        //初始化队尾
        tail = new AtomicReference<>();
        myNode = ThreadLocal.withInitial(new Supplier<QNode>() {
            @Override
            public QNode get() {
                return new QNode();
            }
        });
        myNode.get().lock=false;
        preNode = ThreadLocal.withInitial(() -> null);
        count = ThreadLocal.withInitial(AtomicInteger::new);
        tail.set(myNode.get());
    }

    //将线程加到队尾去,如何实现重入?如果myNode有了,就不管了
    @Override
    public void lock() {
        QNode node = myNode.get();//这里不可能为null,因为设置了初始值
        node.lock = true;
        //如果没有重入过,也就是第一次进来
        if (count.get().get()==0){
            //获取前驱,而后尾结点指向本节点
            QNode pre = tail.getAndSet(myNode.get());
            //设置它的前驱
            preNode.set(pre);
            count.get().incrementAndGet();
        }else{
            //如果重入过,
            //如果要吧它调到队伍最后,那么要通过遍历的方式,将它从链表中退出,加到链表最后
            //...
        }
        //判断是否有前驱,如果有前驱,如果前驱要lock则自旋
        //注意这里直接引用前驱的Node

        if (preNode.get() != null){
            while (preNode.get().lock) {
                //进入自旋
            }
        }
    }

    @Override
    public void unlock() {
        //lock变为false,释放自己
        if(count.get().get() > 0){
            count.get().decrementAndGet();
        }else{
            myNode.get().lock = false;
            //回收方案,将当前节点引用置为自己的preNode
            myNode.set(preNode.get());
        }
    }


    public static void main(String[] args) {
        CLHLock clhLock = new CLHLock();

        for (int i = 0; i < 30; i++) {
            //开30个线程,都重入一次
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("当前线程 "+Thread.currentThread()+" 请求获得锁");
                    try {
                        clhLock.lock();
                        System.out.println("当前线程 "+Thread.currentThread()+" 获得锁");
                        Thread.sleep(500);
                        try{
                            clhLock.lock();
                            System.out.println("重入锁 当前线程 "+Thread.currentThread().getName());
                            Thread.sleep(500);
                        }finally {
                            clhLock.unlock();
                        }
                    }catch (Exception e){
                        e.printStackTrace();
                    }finally {
                        clhLock.unlock();
                    }

                }
            }).start();
        }

    }

    //静态内部类对外不持有引用
    private static class QNode {
        /**
         * true表示该线程需要获取锁,且不释放锁,为false表示线程释放了锁,且不需要锁
         */
        private volatile boolean lock = true;
    }
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113

例子2:原子类自增Demo

public class ThreadLocalDemo1 {
    public static ThreadLocal<String> threadlocal = new ThreadLocal<String>();

    public static class MyThread extends Thread {
        private static AtomicInteger atomicInt = new AtomicInteger();
        public void run() {
            for(int i=0;i<4;i++) {
                threadlocal.set(atomicInt.addAndGet(1) + "");
//                threadlocal.set(null);
                System.out.println("thread name is :" + this.getName() + " , value  is :" + threadlocal.get());
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            }
    }
    public static void main(String[] args) throws Exception {
        Thread th1 = new MyThread();
        Thread th2 = new MyThread();
        Thread th3 = new MyThread();
        th1.setName("Thread 1");
        th2.setName("Thread 2");
        th3.setName("Thread 3");
        th1.start();
        th2.start();
        th3.start();
    }

}

1234567891011121314151617181920212223242526272829303132

2. ThreadLocal的底层数据结构包含哪些内容?

在这里插入图片描述

  • ThreadLocal 数据结构:

    每个Thread线程内部都有一个ThreadLocalMap,ThreadLocalMap是一个初始化的大小为 16 的 Entry数组,Entry对象用来保存每一个 key-value 键值对,key是ThreadLocal对象,value是泛型的 Object。

    Thread 源码:每个Thread线程内部都有一个ThreadLocalMap

    public class Thread{
        ...
            
        /* ThreadLocal values pertaining to this thread. This map is maintained
         * by the ThreadLocal class. */
        ThreadLocal.ThreadLocalMap threadLocals = null;
    
        /*
         * InheritableThreadLocal values pertaining to this thread. This map is
         * maintained by the InheritableThreadLocal class.
         */
        ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
        
        ...
    }
    123456789101112131415
    

    ThreadLocalMap结构:

在这里插入图片描述

ThreadLocalMap构造函数:

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    		//初始 16 容量
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
    		//设置扩容阈值 ( 参数 * 2 / 3)
            setThreshold(INITIAL_CAPACITY);
}
123456789
  • 调用链:

    Thread --> ThreadLocal --> ThreadLocalMap(ThreadLocal firstKey, Object firstValue) --> Entry[] --> Entry(ThreadLocal k, Object v)

3. ThreadLocalMap的初始大小、加载因子分别是多少?

初始大小为 16,加载因子为 2/3

加载因子:加载因子是衡量哈希表密集程度的一个参数,如果加载因子越大,说明哈希表被装载的越多,出现hash冲突的可能性越大,繁殖,被装载的越少,出现hash冲突的可能性越小,如果过小,内存使用率不高,该值取值应该考虑到内存使用率和hash冲突概率的平衡。

4. ThreadLocal底层用到的Hash算法是什么?

  • ThreadLocal 底层用斐波那契数(黄金分割数)实现 hash 算法

  • ThreadLocal 有属性 HASH_INCREMENT(斐波那契数),当每创建一个ThreadLocal对象,它的下一个 hashcode 就会增长 0x61c88647

    private static final int HASH_INCREMENT = 0x61c88647;
    
    private static int nextHashCode() {
            return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    
    123456
    
  • 用斐波那契数生成Entry环形数组,实现 hash的均匀分布 -> 降低hash冲突发生可能

5. ThreadLocal如何解决Hash冲突?

  • ThreadLocalMap 使用开放地址法(线性探测)解决 hash 冲突,HashMap使用链地址法解决 hash 冲突。
  • ThreadLocalMap 采用开放地址法(线性探测法)解决hash冲突。ThreadLocalMap 根据初始 key 的哈希值确定元素在 table 数组中的位置,如果发现这个位置上已经被其他 key 占用,则利用固定的算法寻找一定步长的下一个位置,依次判断,直至找到能够存放的位置。

6. ThreadLocal底层的扩容机制是什么?

ThreadLocal扩容——量化、弹性扩容;

  • 当 table 中Entry的数量 size >= threshold *3/4 时(size >= cap * 2 / 3 * 3 / 4)(扩容的判断条件),需要对 table 进行 2 倍扩容。扩容原因:降低哈希冲突。
private void rehash(){
    expungeStaleEntries();
    if(size >= threshold - threshold /4) resize();
}
1234
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

    setThreshold(newLen);
    size = count;
    table = newTab;
}
123456789101112131415161718192021222324252627

7. ThreadLocal的get方法的实现流程?

  1. 获取当前线程的 threadLocals变量 (map 结构), 从 threadLocals变量 中获取当前 ThreadLocal 变量对应的 ThreadLocalMap.Entry,非空直接返回对应的 value
  2. 为空时使用默认值null构造ThreadLocalMap.Entry,放到当前线程的threadLocals变量中,下次再get时直接返回ThreadLocalMap.Entry对应的value即可。

入口为 ThreadLocal 的 get():

public T get() {
    //获取当前线程
    Thread t = Thread.currentThread();
   	//获取当前线程的ThreadLocalMap
    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;
        }
    }
    return setInitialValue();
}
123456789101112131415

如果map不为空,进入到 map.getEntry():

private Entry getEntry(ThreadLocal<?> key) {
    //计算下标
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    //如果不为空,而且key一致
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}
12345678910

解决冲突问题的开放地址法去寻找Entry

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    //按照已订好的线性探测法逐个寻找
    while (e != null) {
        ThreadLocal<?> k = e.get();
        //如果找到一样的key直接返回
        if (k == key)
            return e;
        //如果发现key为null,expungeStaleEntry(i)通过重新散列所有可能发生冲突的项来删除位于i和下一个空槽之间的陈旧的项
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}
12345678910111213141516171819

如果ThreadLocal获得的map为空,则调用 setInitialValue()去使用默认值null构造ThreadLocalMap.Entry,放到当前线程的threadLocals中

private T setInitialValue() {
    //默认null的value
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}
1234567891011

8. ThreadLocalMap的key是强引用,还是弱引用?为什么?

在这里插入图片描述

  • ThreadLocalMap的 key 是弱引用,弱引用可保证 ThreadLocal 实例对象在每次 GC 时候都能得到清除以释放其占用的内存空间。

9. ThreadLocalMap中的key可能过期么?set、get可能会清理过期key的相关Entry么?

ThreadLocalMap 的 key 可能会过期。由于 ThreadLocalMap 的 key 是弱引用类型,所以可能被 GC 自动回收,从而导致 key 为 null,但该槽对应的Entry并不一定被回收,value 不一定被回收。

过期key对应Entry的清理:

  • 在调用 get 和 set方法的过程中,都可能会触发清理过期key对应的Entry。
  • 在 get方法中可能的调用链: get --> getEntry --> getEntryAfterMiss --> expungeStaleEntry
  • set方法中可能的调用链:
    1. replaceStaleEntry --> cleanSomeSlots(expungeStaleEntry(slotToExpunge),len);
    2. cleanSomeSlots --> expungeStaleEntry
  • expunge : 清除, stale : 过期的, slot : 槽
  • remove rehash等也会有到清理过期key的操作。

10. ThreadLocal的set方法的实现流程?

set方法实现流程的关键点:清理过期key、扩容、设置值

  1. 根据 hash 值和数组长度 求元素放置的位置,即数组下标
  2. 从第一步得出的下标开始遍历,如果key相等,覆盖value,如果key为null,用新的key、value覆盖,同时清理历史key = null 的陈旧数据
  3. 如果当 table 中元素的数量 size >= threshold,遍历 table 并删除 key为null的元素,如果删除后 size>=threshold * 3 /4时,需要对 table 进行 2 倍扩容,把老数据重新哈希散列rehash到新的table中。

ThreadLocal的set方法:

 public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
12345678

进入到ThreadlocalMap的set方法:

private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
    		//根据 hash 值和数组长度 求元素放置的位置,即数组下标
            int i = key.threadLocalHashCode & (len-1);

    		//从第一步得出的下标开始遍历
            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;
                }

                //如果key为null,用新的key、value覆盖
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
    
    		//启发式的扫描并清除过期的key 清理历史key = null 的陈旧数据
    		//如果没有需要清除的并且需要扩容,则进行扩容
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
1234567891011121314151617181920212223242526272829303132

先看到最后的清除过期key的方法 cleanSomeSlots:

private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                // 从位置i的下一个开始搜索
                i = nextIndex(i, len);
                Entry e = tab[i];
                // 如果遇到过期的Entry,清除
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
    		// 只要有过期Entry被移除就会返回true
            return removed;
        }
123456789101112131415161718

看到里面的rehash()方法,就是清理过期的Entry,并且可能的话还会触发resize

private void rehash() {
            expungeStaleEntries();

            if (size >= threshold - threshold / 4)
                resize();
}
123456

11. 如何防止ThreadLocal发生内存泄漏?

内存泄漏的原因:

  • ThreadLocal 里面使用了一个存在弱引用的ThreadLocalMap,当时放掉 ThreadLocal的强引用后(该ThreadLocal只剩下map的key的弱引用),ThreadlocalMap里面的 value 却没有被回收,而这块 value 永远不会被访问到了,所以会存在内存泄漏。

如何防止内存泄漏:

  • 在使用完毕后,及时调用remove方法。Entry继承弱引用,但只能实现对Reference的key的回收,而对value的回收则需要手动解决。remove方法中有将其应用置空null的操作,断掉value的强引用使其可被回收。

我们看到ThreadLocal的remove方法,这是主动remove掉以自己这个对象为key的Entry:

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

实际是调用ThreadLocalMap的remove操作:

 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();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }
1234567891011121314

e.clear():

public void clear() {
        this.referent = null;// Reference<T>中的 referent变量
    }
123

除了通过e.clear()让key引用的ThreadLocal置空,还通过expungeStaleEntry方法去删除key == null的槽

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

    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    // 将空槽后面的有实际意义的Entry前移(rehash)
    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;

                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}
12345678910111213141516171819202122232425262728293031323334

可以利用jvisulvm来检测内存泄漏

12. ThreadLocal的应用场景有哪些?

  1. 线程安全: ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,确保了线程安全。
  2. 参数传递: ThreadLocal 用作每个线程内需要独立保存上下文参数,以便供其他方法更方便地获取这些上下文参数。 ThreadLocal设计模式/上下文设计模式 。特别的,这种应用在多层架构中,使用比较多。例如:在同一个线程的不同开发层次中共享数据。 如 Web应用开发中表示层、业务层、持久层需要共享数据等。

使用ThreadLocal的注意事项:

  1. 不要使用 ThreadLocal 存储大对象
  2. 注意使用 ThreadLocal 的 remove 方法,清理过期数据,否则会产生大量脏数据,发生内存泄漏等问题。
  3. 使用 private final static 进行修饰,防止多实例时内存的泄漏问题。(可能丢失之前的引用,导致之前那个ThreadLocal对象对应的Entry无法调用remove方法去清理过期数据)

13. ThreadLocal和synchronized有什么区别?

  1. synchronized 的资源是多个线程共享的,所以访问的时候需要加锁。
  2. ThreadLocal 是每个线程都有一个副本(作为map中的key,这个map是每个线程维护一个的),是不需要加锁的。
  3. synchronized 是 时间换空间。
  4. ThreadLocal 是 空间换时间。

标签:线程,get,ThreadLocal,理解,深入,key,Entry,null
From: https://www.cnblogs.com/javaxubo/p/18050363

相关文章

  • 理解大模型中的 d_model
    在深度学习和Transformer模型的上下文中,d_model中的“d”通常代表“dimension”,即“维度”的简写。因此,d_model指的是模型中向量的维度大小,这是一个关键的参数,影响着模型的性能和计算复杂度。在Transformer架构中,d_model特别指向嵌入向量的维度,以及模型内部传递的数据向量的统一维......
  • 对梯度下降法中参数更新是减去学习率与偏导数之积而不是学习率与偏导数的倒数之积的理
    这是我在对比softmax回归和线性回归偏导时的一个疑问,看到知乎上有一个人同样的问题,问题链接为:https://www.zhihu.com/question/263929081。原回答里,我非常认可的一个回答是:我的理解是这两种看法都是正确的,分别衍生出不同的优化方法。首先是除以梯度,这是利用了泰勒展开式,从导数......
  • ThreadLocal解析
    ThreadLocal解析目录ThreadLocal解析1.两大使用场景——ThreadLocal的用途典型场景1:每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random)典型场景2:每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻......
  • Python 中的 if __name__ == '__main__' 该如何理解
    结论if__name__=='__main__'我们简单的理解就是:如果模块是被直接运行的,则代码块被运行,如果模块是被导入的,则代码块不被运行。程序入口对于很多编程语言来说,程序都必须要有一个入口,比如C,C++,以及完全面向对象的编程语言Java,C#等。如果你接触过这些语言,对于程序入口这个概......
  • 说说你对vue的mixin的理解,有什么应用场景?
    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助一、mixin是什么Mixin是面向对象程序设计语言中的类,提供了方法的实现。其他类可以访问mixin类的方法而不必成为其子类Mixin类通常作为功能模块使用,在需要该功能时“混入”,有利于代码复用又避免了多继承的复杂Vue......
  • C++ 拷贝构造函数(初学有点难理解)
    拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于:通过使用另一个同类型的对象来初始化新创建的对象。复制对象把它作为参数传递给函数。复制对象,并从函数返回这个对象。如果在类中没有定义拷......
  • 深入浅出Gitlab Runner自动构建C#应用程序
    概述程序员签入代码到Gitlab之后,GitlabRunner自动从流水线领取作业。按我们编排的“作业”,流水线工作步骤如下:程序员在Windows的VisualStudio2022中签入了“解决方案A”到Gitlab;Gitlab根据我们编排的.gitlab-ci.yml创建“流水线”;GitlabRunner领取到“作业”,以指定映像......
  • 深入浅出Go语言:泛型入门指南
    深入浅出Go语言:泛型入门指南原创 麻凡 麻凡 2024-03-0109:00 湖南 听全文随着Go1.18版本的发布,泛型正式成为了Go语言的一部分。泛型为Go开发者带来了更强大的类型抽象能力,允许我们编写更加灵活和可复用的代码。本文将带你了解Go泛型的基础知识,让你快速上手这一新特......
  • 破局数据分析滞后难题,赋能企业高速增长的指标管理解决方案
    指标是什么?业务发展过程中,企业内外部都会产生很多的业务数据,对这些数据进行采集、计算、落库、分析后,形成的统计结果称为指标。简单来说,指标是业务被拆解、量化后形成的数量特征,企业利用数据指标对业务进行精准的号脉,实现对业务的科学管理和有效优化。在我们对多家企业展开深入......
  • 如何理解IOC中的“反转”和DI中的“注入”
    在理解IOC中的“反转”和DI中的“注入”之前,首先要理解原本的控制流程。在传统的应用程序中,对象之间的依赖关系通常由调用方(例如客户端或者上层模块)来管理。这意味着,当一个对象需要另一个对象时,它必须自己创建或查找依赖的对象,这种控制权在对象之间的依赖关系的代码中是显式......