• Java 并发 - ThreadLocal详解


    带着BAT大厂的面试问题去理解

    • 什么是ThreadLocal? 用来解决什么问题的?
    • 说说你对ThreadLocal的理解
    • ThreadLocal是如何实现线程隔离的?
    • 为什么ThreadLocal会造成内存泄露? 如何解决
    • 还有哪些使用ThreadLocal的应用场景?

    ThreadLocal简介

    ThreadLocal本地线程变量,线程自带的变量副本(实现了每一个线程副本都有一个专属的本地变量,主要解决的就是让每一个线程绑定自己的值,自己用自己的,不跟别人争抢。通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全的问题)

    该类提供线程局部变量。 这些变量与它们的正常对应物的不同之处在于,访问其中的每个线程(通过其getset方法)具有其自己的,独立初始化的变量副本。 ThreadLocal实例通常是希望将状态与线程相关联的类中的私有静态字段(例如,用户ID或事务ID)。

    ThreadLocal API

    构造方法 

    public ThreadLocal()       创建一个线程局部变量。

    方法详细

    1. ①. protected T initialValue​():initialValue():返回此线程局部变量的当前线程的"初始值"
    2. (对于initialValue()较为老旧,jdk1.8又加入了withInitial()方法)
    3. ②. static ThreadLocal withInitial​(Supplier supplier):创建线程局部变量
    4. ③. T get​():返回当前线程的此线程局部变量的副本中的值
    5. ④. void set​(T value):将当前线程的此线程局部变量的副本设置为指定的值
    6. ⑤. void remove​():删除此线程局部变量的当前线程的值
    7. /**
    8. * ThreadLocal 初始化方法 不初始化则为 null
    9. */
    10. ThreadLocal threadLocal = ThreadLocal.withInitial(() -> 0);

    代码总结

    阿里ThreadLocal规范

    ThreadLocal源码分析

    • ThreadLocalMap (Entry[]数组)中存放的是一个个的 Entry节点,它有两个属性字段,弱引用 key(ThreadLocal对象) ,和强引用 value (当前线程变量副本的值)。
    • JDK8 中,每个线程对象 Thread 类内部都有一个成员属性 threadLocals(即ThreadLocalMap,它是一个Entry[]数组,而不是 Map 集合哦~),各个线程在调用同一个 ThreadLocal 对象的set(value)方法设置值的时候,就是往各自的 ThreadLocalMap 对象数组中新增值。

    Thread|ThreadLocal|ThreadLocalMap关系

     

     

    上图中基本描述出了Thread、ThreadLocalMap以及ThreadLocal三者之间的包含关系。Thread类对象中维护了ThreadLocalMap成员变量,而ThreadLocalMap维护了以ThreadLocal为key,需要存储的数据为value的Entry数组,这是它们三者之间的基本包含关系。

    1. 当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放
    2. t.threadLocals = new ThreadLocalMap(this, firstValue) 如下这行代码,可以知道每个线程都会创建一个ThreadLocalMap对象,每个线程都有自己的变量副本
    3. Thread类中有一个ThreadLocal.ThreadLocalMap threadLocals = null的变量,ThreadLocal相当于是Thread类和ThreadLocalMap的桥梁,在ThreadLocal中有静态内部类ThreadLocalMap,ThreadLocalMap中有Entry数组

    ThreadLocalMap对象是什么

    本质上来讲, 它就是一个Map, 但是这个ThreadLocalMap与我们平时见到的Map有点不一样

    • 它没有实现Map接口;
    • 它没有public的方法, 最多有一个default的构造方法, 因为这个ThreadLocalMap的方法仅仅在ThreadLocal类中调用, 属于静态内部类
    • ThreadLocalMap的Entry实现继承了WeakReference>
    • 该方法仅仅用了一个Entry数组来存储Key, Value; Entry并不是链表形式, 而是每个bucket里面仅仅放一个Entry;

    理解ThreadLocal类set方法

    试想我们一个请求对应一个线程,我们可能需要在请求到达拦截器之后,可能需要校验当前请求的用户信息,那么校验通过的用户信息通常都放入到ThreadLocalMap中,以方便在后续的方法中直接从ThreadLocalMap中获取

    但是我们并没有直接操作ThreadLocalMap来存取数据,而是通过一个静态的ThreadLocal变量来操作,我们从上面的图可以看出,ThreadLocalMap中存储的键其实就是ThreadLocal的弱引用所关联的对象,那么键是如何操作类似HashMap的值的呢?我们一起来分析一下set方法:

    1. public void set(T value) {
    2. // 首先获取调用此方法的线程
    3. Thread t = Thread.currentThread();
    4. // 将线程传递到getMap方法中来获取ThreadLocalMap,其实就是获取到当前线程的成员变量threadLocals所指向的ThreadLocalMap对象
    5. ThreadLocalMap map = getMap(t);
    6. // 判断Map是否为空
    7. if (map != null)
    8. // 如果Map为不空,说明当前线程内部已经有ThreadLocalMap对象了,那么直接将本ThreadLocal对象作为键,存入的value作为值存储到ThreadLocalMap中
    9. map.set(this, value);
    10. else
    11. // 创建一个ThreadLocalMap对象并将值存入到该对象中,并赋值给当前线程的threadLocals成员变量
    12. createMap(t, value);
    13. }
    14. // 获取到当前线程的成员变量threadLocals所指向的ThreadLocalMap对象
    15. ThreadLocalMap getMap(Thread t) {
    16. return t.threadLocals;
    17. }
    18. // 创建一个ThreadLocalMap对象并将值存入到该对象中,并赋值给当前线程的threadLocals成员变量
    19. void createMap(Thread t, T firstValue) {
    20. t.threadLocals = new ThreadLocalMap(this, firstValue);
    21. }

    上面的set方法是ThreadLocal的set方法,就是为了将指定的值存入到指定线程的threadLocals成员变量所指向的ThreadLocalMap对象中,那么具体是如何存取的,其实调用的还是ThreadLocalMap的set方法,源码分析如下所示:

    1. private void set(ThreadLocal> key, Object value) {
    2. // We don't use a fast path as with get() because it is at
    3. // least as common to use set() to create new entries as
    4. // it is to replace existing ones, in which case, a fast
    5. // path would fail more often than not.
    6. Entry[] tab = table;
    7. int len = tab.length;
    8. // 计算当前ThreadLocal对象作为键在Entry数组中的下标索引
    9. int i = key.threadLocalHashCode & (len-1);
    10. // 线性遍历,首先获取到指定下标的Entry对象,如果不为空,则进入到for循环体内,
    11. // 判断当前的ThreadLocal对象是否是同一个对象,如果是,那么直接进行值替换,并结束方法,
    12. // 如果不是,再判断当前Entry的key是否失效,如果失效,则直接将失效的key和值进行替换。
    13. // 这两点都不满足的话,那么就调用nextIndex方法进行搜寻下一个合适的位置,进行同样的操作,
    14. // 直到找到某个位置,内部数据为空,也就是Entry为null,那么就直接将键值对设置到这个位置上。
    15. // 最后判断是否达到了扩容的条件,如果达到了,那么就进行扩容。
    16. for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
    17. ThreadLocal> k = e.get();
    18. if (k == key) {
    19. e.value = value;
    20. return;
    21. }
    22. if (k == null) {
    23. replaceStaleEntry(key, value, i);
    24. return;
    25. }
    26. }
    27. tab[i] = new Entry(key, value);
    28. int sz = ++size;
    29. if (!cleanSomeSlots(i, sz) && sz >= threshold)
    30. rehash();
    31. }

    这里的代码核心的地方就是for循环这一块,代码上面加了详细的注释,这里在复述一遍:

    线性遍历,首先获取到指定下标的Entry对象,如果不为空,则进入到for循环体内,判断当前的ThreadLocal对象是否是同一个对象

    如果是,那么直接进行值替换,并结束方法。如果不是,再判断当前Entry的key是否失效,如果失效,则直接将失效的key和值进行替换。

    这两点都不满足的话,那么就调用nextIndex方法进行搜寻下一个合适的位置,进行同样的操作,直到找到某个位置,内部数据为空,也就是Entry为null,那么就直接将键值对设置到这个位置上。最后判断是否达到了扩容的条件,如果达到了,那么就进行扩容。

    这里有两点需要注意:一是nextIndex方法,二是key失效,这里先解释第一个注意点,第二个注意点涉及到弱引用JVM GC问题。

    nextIndex方法的具体代码如下所示:

    1. private static int nextIndex(int i, int len) {
    2. return ((i + 1 < len) ? i + 1 : 0);
    3. }

    其实就是寻找下一个合适位置,找到最后一个后还不合适的话,那么从数组头部重新开始找,且一定可以找到,因为存在扩容阈值,数组必定有冗余的位置存放当前键值对所对应的Entry对象。其实nextIndex方法就是大名鼎鼎的『开放寻址法』的应用。

    这一点和HashMap不一样,HashMap存储HashEntry对象发生哈希冲突的时候采用的是链表方式进行存储,而这里是去寻找下一个合适的位置,思想就是『开放寻址法』。

    理解ThreadLocal类get方法

    在实际的开发中,我们往往需要在代码中调用ThreadLocal对象的get方法来获取存储在ThreadLocalMap中的数据,具体的源码如下所示:

    1. public T get() {
    2. // 获取当前线程的ThreadLocalMap对象
    3. Thread t = Thread.currentThread();
    4. ThreadLocalMap map = getMap(t);
    5. if (map != null) {
    6. // 如果map不为空,那么尝试获取Entry数组中以当前ThreadLocal对象为键的Entry对象
    7. ThreadLocalMap.Entry e = map.getEntry(this);
    8. if (e != null) {
    9. // 如果找到,那么直接返回value
    10. @SuppressWarnings("unchecked")
    11. T result = (T)e.value;
    12. return result;
    13. }
    14. }
    15. // 如果Map为空或者在Entry数组中没有找到以当前ThreadLocal对象为键的Entry对象,
    16. // 那么就在这里进行值初始化,值初始化的过程是将null作为值,当前ThreadLocal对象作为键,
    17. // 存入到当前线程的ThreadLocalMap对象中
    18. return setInitialValue();
    19. }
    20. // 值初始化过程
    21. private T setInitialValue() {
    22. T value = initialValue();
    23. Thread t = Thread.currentThread();
    24. ThreadLocalMap map = getMap(t);
    25. if (map != null)
    26. map.set(this, value);
    27. else
    28. createMap(t, value);
    29. return value;
    30. }

    值初始化过程是这样的一个过程,如果调用新的ThreadLocal对象的get方法,那么在当前线程的成员变量threadLocals中必定不存在key为当前ThreadLocal对象的Entry对象,那么这里值初始话就将此ThreadLocal对象作为key,null作为值存储到ThreadLocalMap的Entry数组中。

    理解ThreadLocal的remove方法

    使用ThreadLocal这个工具的时候,一般提倡使用完后及时清理存储在ThreadLocalMap中的值,防止内存泄露。这里一起来看下ThreadLocal的remove方法。

    1. public void remove() {
    2. ThreadLocalMap m = getMap(Thread.currentThread());
    3. if (m != null)
    4. m.remove(this);
    5. }
    6. // 具体的删除指定的值,也是通过遍历寻找,找到就删除,找不到就算了
    7. private void remove(ThreadLocal key) {
    8. Entry[] tab = table;
    9. int len = tab.length;
    10. int i = key.threadLocalHashCode & (len-1);
    11. for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
    12. if (e.get() == key) {
    13. e.clear();
    14. expungeStaleEntry(i);
    15. return;
    16. }
    17. }
    18. }

    看了这么多ThreadLocal的源码实现,其实原理还是很简单的,基本上可以说是一看就懂,理解ThreadLocal原理,其实就是需要理清Thread、ThreadLocal、ThreadLocalMap三者之间的关系

    这里加以总结:线程类Thread内部持有ThreadLocalMap的成员变量,而ThreadLocalMap是ThreadLocal的内部类,ThreadLocal操作了ThreadLocalMap对象内部的数据,对外暴露的都是ThreadLocal的方法API,隐藏了ThreadLocalMap的具体实现,理清了这一点,ThreadLocal就很容易理解了。

    理解ThreadLocalMap内存泄露问题

    这里所说的ThreadLocal的内存泄露问题,其实都是从ThreadLocalMap中的一段代码说起的,这段代码就是Entry的构造方法:

    1. static class Entry extends WeakReference> {
    2. /** The value associated with this ThreadLocal. */
    3. Object value;
    4. Entry(ThreadLocal k, Object v) {
    5. super(k);
    6. value = v;
    7. }
    8. }

    这里简单介绍一下Java内的四大引用:

    强引用

     软引用

    弱引用 

    虚引用 

    为什么源代码用弱引用?key为null的entry,原理解析?

    当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录,这个记录的键为ThreadLocal的弱引用,value就是通过set设置的值,这个value值被强引用。

    如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,是不会释放的,就会造成内存泄漏。

    考虑这个ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在垃圾回收的时候就被回收,但是对应的value还是存在的这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。

     

    总结:ThreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLocal依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉,但是对应的值不会被回收,这个时候Map中就可能存在key为null但是值不为null的项,所以在使用ThreadLocal的时候要养成及时remove的习惯。

    ThreadLocal如何实现线程隔离

    • 首先获取当前线程对象t, 然后从线程t中获取到ThreadLocalMap的成员属性threadLocals
    • 如果当前线程的threadLocals已经初始化(即不为null) 并且存在以当前ThreadLocal对象为Key的值, 则直接返回当前线程要获取的对象;
    • 如果当前线程的threadLocals已经初始化(即不为null)但是不存在以当前ThreadLocal对象为Key的的对象, 那么重新创建一个对象, 并且添加到当前线程的threadLocals Map中,并返回
    • 如果当前线程的threadLocals属性还没有被初始化, 则重新创建一个ThreadLocalMap对象, 并且创建一个对象并添加到ThreadLocalMap对象中并返回。

    1. public T get() {
    2. Thread t = Thread.currentThread();
    3. ThreadLocalMap threadLocals = getMap(t);
    4. if (threadLocals != null) {
    5. ThreadLocalMap.Entry e = threadLocals.getEntry(this);
    6. if (e != null) {
    7. @SuppressWarnings("unchecked")
    8. T result = (T)e.value;
    9. return result;
    10. }
    11. }
    12. return setInitialValue();
    13. }

    如果存在则直接返回很好理解, 那么对于如何初始化的代码又是怎样的呢?

    1. private T setInitialValue() {
    2. T value = initialValue();
    3. Thread t = Thread.currentThread();
    4. ThreadLocalMap map = getMap(t);
    5. if (map != null)
    6. map.set(this, value);
    7. else
    8. createMap(t, value);
    9. return value;
    10. }

    那么我们看过代码之后就很清晰的知道了为什么ThreadLocal能够实现变量的多线程隔离了; 其实就是用了Map的数据结构给当前线程缓存了, 要使用的时候就从本线程的threadLocals对象中获取就可以了, key就是当前线程;

    当然了在当前线程下获取当前线程里面的Map里面的对象并操作肯定没有线程并发问题了, 当然能做到变量的线程间隔离了;

    ThreadLocalMap 对象是何时第一次被创建呢?

    • 每个线程 Thread 对象的 ThreadLocalMap 都是延迟初始化的,当我们在调用 ThreadLocal 对象的 set() 或 get()方法时,它会检测当前线程是否已经绑定了 ThreadLocalMap,如果已经绑定,则继续执行 set() 或 get()方法的逻辑。
    • 而如果没有,则会先创建 ThreadLocalMap 并将其绑定给 Thread 对象。

      那么线程的 ThreadLocalMap 会被多次创建吗?

    • 不会,在线程的生命周期内,ThreadLocalMap 对象只会被初始化一次。

    ThreadLocalMap 的初始化长度是多少呢?那为什么初始容量要是2的N次幂数呢?

    • 初始化时,ThreadLocalMap 容量为 16
    • 这个设计它和 HashMap 是一样的,目的都是为了方便 hash 寻址时,得到的 index (桶位)更均匀分布,减少 hash 冲突
    • 寻址算法为:index = threadLocalHashCode & (table.length - 1)。这个算法实际就是取模运算:hash % tab.length,而计算机中直接求余运算效率不如位移运算
    • 所以源码中做了优化,使用 hash & (tab.length- 1)来寻找桶位。而实际上 hash % length 等于 hash & ( length - 1) 的前提是 length 必须为 2 的 n 次幂

    为什么 ThreadLocalMap 选择去重新设计"Map",而不直接使用 JDK中的 HashMap呢?

    因为 ThreadLocal 自己重新设计的 Map,它可以把自己的 Key 限定为特有类型(ThreadLocal),这个特定类型的Key 使用的是弱引用 WeakReference>,而 HashMap 中的 Key 采用的是强引用方式。

    ThreadLocal总结

    • ①. ThreadLocal本地线程变量,以空间换时间,线程自带的变量副本,人手一份,避免了线程安全问题

    • ②. 每个线程持有一个只属于自己的专属Map并维护了Thread Local对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题

    • ③. ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题

    • ④. 都会通过expungeStaleEntry,cleanSomeSlots, replace StaleEntry这三个方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏,属于安全加固的方法

    • ⑤. 用完之后一定要remove操作

     

  • 相关阅读:
    鸿鹄工程项目管理系统em Spring Cloud+Spring Boot+前后端分离构建工程项目管理系统
    python与neo4j
    Vue 中 v-if 和 v-show 有什么区别?
    Leetcode 1775. 通过最少操作次数使数组的和相等
    SCS RC翠鸟回收成分认证是什么?如何申请?需要什么
    线下门店如何根据员工排班情况给客户预约
    Mac虚拟机Parallels Desktop 20 for Mac破解版发布 完整支持 Windows 11
    Spring声明式事务管理(基于XML方式实现)
    Kafka MQ 如何处理请求
    Linux安装jrockit-jdk1.6.0_29-R28.2.0-4.1.0-linux-x64
  • 原文地址:https://blog.csdn.net/weixin_63566550/article/details/125980026