ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

透彻理解ThreadLocal

2021-07-04 13:34:08  阅读:179  来源: 互联网

标签:对象 threadLocal value ThreadLocal 理解 线程 内存 entry 透彻


作用和使用场景

  • threadLocal是线程本地储存,在一个线程中通过threadLocal.set()方法赋值一个对象T(假设是T@48533e64),在赋值后当前线程内通过threadLocal.get()方法得到的值都是T@48533e64对象,以便后续处理时用到。使用场景:跨类跨方法传递数据。假如你再A类计算得到一个值,而在C类的某个方法中要用到该值,那么需要在C类的该方法上新增一个参数用于传递该值,但是使用threadLocal后,就可以省略这个参数,直接通过threadLocal.get()获取得到。

  • 每个线程设定的对象与其他线程是隔离的,线程安全控制使用场景:典型的如多个线程与数据库交互时,每个线程都拥有各自的Connections对象;再比如在对日期进行格式化的工具类中,如果用静态的SimpleDateFormat成员变量去做格式化,那么可能会发生错误,可以通过静态的ThreadLocal<SimpleDateFormat>处理解决。

内存模型图

看如下代码示例及其对应的内存模型图

public class ThreadLocalTest {
    @Test
    public void test() {
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        Thread thread = new Thread(() -> threadLocal.set("value"));
        thread.start();
    }    
}

ThreadLocalMap

从内存图知,ThreadLocalMap是一个容器类,key是threadLocal对象,value是我们要保存的对象,threadLocal赋值的key和value都是保存在各自线程的threadLocalMap对象中的。threadLocalMap对象底层也是entry数组(注意这个entry和hashMap中的entry不是同一个类),而与HashMap不同的是,他们解决hash冲突的方式不一样,threadLocalMap使用开放寻址法(线性探测),而hashMap使用的是链地址法

  • 开放寻址法(线性探测)

    当我们往散列表中插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,我们就从当前位置开始,依次往后查找,看是否有空闲位置,直到找到为。

    缺点:当散列表中插入的数据越来越多时,散列冲突发生的可能性就会越来越大,空闲位置会越来越少,线性探测的时间就会越来越久。极端情况下,我们可能需要探测整个散列表,所以最坏情况下的时间复杂度为 O(n),因此该方法适用于存储少量元素的hash表

    为什么threadLocalMap使用线性探测方法?

    答:我们在编码时,大多数情况下只设置了一个或几个threadLocal对象,不会有太多threadLocal对象,因此由用户编码而存入到threadLocalMap中的元素大多也就一个或几个而已,因此threadLocalMap存放的元素数量很少,适合使用线程探测法来解决hash冲突

  • 链地址法

    每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向  链表连接起来

ThreadLocal源码详解

get方法

/**
  * Returns the value in the current thread's copy of this
  * thread-local variable.  If the variable has no value for the
  * current thread, it is first initialized to the value returned
  * by an invocation of the {@link #initialValue} method.
  *
  * @return the current thread's value of this thread-local
  */
public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 得到当前线程的threadLocals属性对象(即Entry[]对象)
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 以threadLocal对象做为key,通过“开放定址法”去寻找对应位置的entry
        // 特别注意:在寻找过程中,会对遇到的无效entry进行清除以避免内存泄漏
        // 解释:"清除无效entry"即打断如内存图所示的”引用7“和”引用5“的引用
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 若该entry对象不为null则返回该entry.value
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 若当前线程的threadLocals属性对象为null,或者未找对应的entry,返回初始化的值
    return setInitialValue();
}

set方法

/**
 * Sets the current thread's copy of this thread-local variable
 * to the specified value.  Most subclasses will have no need to
 * override this method, relying solely on the {@link #initialValue}
 * method to set the values of thread-locals.
 *
 * @param value the value to be stored in the current thread's copy of
 *        this thread-local.
 */
public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 得到当前线程的threadLocals属性对象(即Entry[]对象)
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 以threadLocal对象做为key,通过“开放定址法”去寻找对应位置的entry,若找到正确的entry,则直接对该entry.value赋值,若未找到则new一个新的entry放在正确的位置。
        // 特别注意:在寻找entry的过程中,会对遇到的无效entry进行清除避免内存泄漏。必要时会进行扩容(扩容前会清除所有的无效entry再判断是否需要扩容)
        // 解释:"清除无效entry"即打断如内存图所示的”引用7“和”引用5“的引用
        map.set(this, value);
    else
        // 新建一个ThreadLocalMap对象,赋值到该线程t的threadLocals属性上,然后将key(threadLocal对象)和value对象放到map中正确的位置
        createMap(t, value);
}

remove方法

public void remove() {
    // 得到当前线程的threadLocals属性对象(即Entry[]对象)
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        // 相当于将内存图中的”引用5、引用6、引用7“全部指向null
        m.remove(this);
}

内存泄漏问题

内存泄漏现象是存在无用的且一直无法被垃圾回收器GC掉的对象。而使用threadLocal涉及到的内存泄漏指的是各个线程中threadLcoalMap属性对象的entry数组引用到的一些无用的对象(即无效的entry)。这里分如下2个方面来讲

  • 一、threadLocal生命周期比线程的生命周期短(同时也解释了Entry为什么要用弱引用) 根据内存图,在threadLocal生命周期结束后,"引用1"断开,threadLocal对象只会被“弱引用6”引用到,由于是弱引用,GC时,threadLocal对象将会被回收,此时该entry对应的key就是null了,对于这种entry我称呼其为无效entry。这时该无效entry以及它引用的value对象都是无用的对象,但此时还占用的内存。不过在该线程中,一旦有其他的threadLocal对象在进行get或set等操作时,根据之前【ThreadLocal源码详解】可知,该线程的threadLocalMap对象中的无效entry就有可能会被清除掉,尤其在其扩容时,无效entry一定会被清除掉。故这种情况下是不会产生内存泄漏的。

    • 思考:如果"引用6"不是弱引用而是强引用,那么就无法判断threadLocalMap中哪些是无效entry对象,就不能有针对性的进行清除操作,从而导致内存泄漏了。这也是java.lang.ThreadLocal.ThreadLocalMap.Entry类在设计时继承WeakReference类使用弱引用的原因。

  • 二、threadLocal生命周期比线程的生命周期长

    该情况也是我们编码时的常用情况,一般将threadLocal申明为静态的常量的成员变量,供各个线程去保存自己的数据。 当线程的生命周期结束后,内存图中的”引用2“断开,此时thread对象及其直接或间接引用的对象都变成了GC Root不可达对象(在内存图中指的是“thread对象、threadLcoalMap对象、entry[]对象、entry对象、value对象),即变成了垃圾,在GC后这些垃圾对象都会被回收掉,因此这种情况也不会产生内存泄漏。

    特别注意:由于我们在实际开发过程中,我们一般是在这些线程中使用threadLocal。1)处理API接口的主线程;2)线程池中获取的线程。以上2种线程都有一个特点,那就是在处理完一个请求之后线程不会立即消亡,会被放到线程池中以便处理后续的请求(即线程的复用,当然可以配置超过指定的空闲时间线程消亡)。既然不会立即消亡,那么该线程所引用到所有entry都没办法被垃圾回收器回收,此时就产生了内存泄漏;更糟糕的是,该线程在处理上一次请求时赋值到threadLocal中的数据,在下一请求处理时依然会被获取到,而这个值对于这次的请求来说是一个脏值,即读取了脏数据。因此要解决中这种情况下导致的内存泄漏和 脏读的问题,那么在线程每一次处理完请求之后都需要调用threadLocal.remove()方法。

  • 内存泄漏问题总结

    通过上面两种情况的分析知道,只要对应的线程消亡或者threadLocal对象消亡,不管有没有调用remvoe方法都不会发生内存泄漏问题。只有当线程和threadLocal都不消亡时才会存在内存泄漏问题,而这种情况一般都是将threadLocal对象设置为静态常量类型,在线程池中使用。而线程池,我们一般都会配置最大活动线程数(即线程池中最多活动的线程)和空闲超时时间(即空闲线程超过这个时间会消亡),随着线程池中活动线程的数量上升直到达到最大线程活动数量时,有关threadLocal所产生的内存占用也会随之上升并达到一个顶值,即使没有调用remove方法,那么产生的内存泄漏也不会无上限的增加,危害不算特别大,只是这种情况下的脏读问题是一个严重的问题,因此这种情况一定要调用remove方法避免产生脏读(同时也避免了内存泄露问题),即对于静态常量的threadLoca在线程池(不管是tomcat线程池还是自己构造的线程池)中进行操作完成后一定要调用remove方法以避免产生脏读和内存泄露

标签:对象,threadLocal,value,ThreadLocal,理解,线程,内存,entry,透彻
来源: https://blog.csdn.net/zhukai_boke/article/details/118461202

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有