ThreadLocal源码学习笔记

参考文章:

概述

线程安全问题的核心在于多个线程会对同一个临界区共享资源进行操作,如果每个线程都使用自己的“共享资源”,各自使用各自的,又互相不影响到彼此即让多个线程间达到隔离的状态,这样就不会出现线程安全的问题。这就是一种“空间换时间”的方案,每个线程都会都拥有自己的“共享资源”无疑内存会大很多,但是由于不需要同步也就减少了线程可能存在的阻塞等待的情况从而提高的时间效率。

从ThreadLocal这个类名可以顾名思义的进行理解,表示线程的“本地变量”,即每个线程都拥有该变量副本,达到人手一份的效果,各用各的这样就可以避免共享资源的竞争。

源码阅读

ThreadLocal.set()
public void set(T value) {
    //1. 获取当前线程实例对象
    Thread t = Thread.currentThread();
    //2. 通过当前线程实例获取到ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //3. 如果Map不为null,则以当前threadLocl实例为key,值为value进行存入
        map.set(this, value);
    else
        //4.map为null,则新建ThreadLocalMap并存入value
        createMap(t, value);
}

可以看到ThreadLocal对象的set方法的作用过程是:获取当前线程->再获取依附在当前线程上的threadLocalMap对象->threadLocalMap再根据当前threadLocal对象为key去获取value。

threadLocalMap

他是Thread对象的一个属性,用于存放ThreadLocalMap.Entry(ThreadLocal key,Object value)。其中key是当前的threadLocal对象的弱引用,这里可能导致ThreadLocal出现内存泄漏(

当threadLocal外部强引用被置为null( threadLocalInstance=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没有引用链可达,在垃圾回收的时候都会被系统进行回收。在实际开发中,会使用线程池去维护线程的创建和复用,比如固定大小的线程池,线程为了复用是不会主动结束的,所以,threadLocal的内存泄漏问题,是应该值得我们思考和注意的问题。

作者:你听_

链接:https://juejin.im/post/5aeeb22e6fb9a07aa213404a

来源:掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

threadLocalMap.总体大概实现

没有实现map接口,但是实现了类似的功能。

它内部有一个Entry数组,也是用到了key.hashCode去给要插入的key-value在数组中分配位置,但当遇到了hash冲突的时候,会用到开放地址法去解决,下面解释了什么是开放地址法。

threadLocalMap.set()实现
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
    int len = tab.length;
    //根据threadLocal的hashCode确定Entry应该存放的位置
    int i = key.threadLocalHashCode & (len-1);
    //采用开放地址法,hash冲突的时候使用线性探测
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        //覆盖旧Entry
        if (k == key) {
            e.value = value;
            return;
        }
        //当key为null时,说明threadLocal强引用已经被释放掉,那么就无法
        //再通过这个key获取threadLocalMap中对应的entry,这里就存在内存泄漏的可能性
        if (k == null) {
            //用当前插入的值替换掉这个key为null的“脏”entry
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    //新建entry并插入table中i处
    tab[i] = new Entry(key, value);
    int sz = ++size;
    //插入后再次清除一些key为null的“脏”entry,如果大于阈值就需要扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

作者:你听___
链接:https://juejin.im/post/5aeeb22e6fb9a07aa213404a
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
threadLocal.hashCode实现
 = nextHashCode.getAndAdd(0x61c88647);

这里的nextHashCode为AtomicInteger类型,0x61c88647这个数是有特殊意义的,它可以使 hashcode 均匀的分布在大小为 2 的 N 次方的数组里,这是Fibonacci Hashing,关于更多介绍可以看这篇文章的threadLocal散列值部分。也正是能够均匀分布,所以threadLocal选择使用开放地址法来解决hash冲突的问题。

threadLocalMap.确定新值插入到哈希表中的位置

利用当前key(即threadLocal实例)的hashcode与哈希表大小相与,因为哈希表大小总是为2的幂次方,所以相与等同于一个取模的过程,这样就可以通过Key分配到具体的哈希桶中去。而至于为什么取模要通过位与运算的原因就是位运算的执行效率远远高于了取模运算

threadLocalMap.怎样解决“脏”Entry

在分析threadLocal,threadLocalMap以及Entry的关系的时候,我们已经知道使用threadLocal有可能存在内存泄漏(对象创建出来后,在之后的逻辑一直没有使用该对象,但是垃圾回收器无法回收这个部分的内存),在源码中针对这种key为null的Entry称之为“stale entry”,直译为不新鲜的entry,我把它理解为“脏entry”,自然而然,Josh Bloch and Doug Lea大师考虑到了这种情况,在set方法的for循环中寻找和当前Key相同的可覆盖entry的过程中通过replaceStaleEntry方法解决脏entry的问题。如果当前table[i]为null的话,直接插入新entry后也会通过cleanSomeSlots来解决脏entry的问题。

threadLocalMap.扩容

新建一个大小为原来数组长度的两倍的数组,然后遍历旧数组中的entry并将其插入到新的hash数组中,主要注意的是,在扩容的过程中针对脏entry的话会令value为null,以便能够被垃圾回收器能够回收,解决隐藏的内存泄漏的问题。

threadLocalMap.环形查找Hash冲突后的地址

在set时,

for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {}

采用开放地址法,e = tab[i = nextIndex(i,len)]实现了环形遍历,实现是比较i+1和len的大小,如果遍历到了最后一个(i+1=len)就返回0,否则就返回 i+1。

对于散列冲突,一般有两种解决方法

  1. 分离链表法,即HashMap中用到的方法。
  2. 开放地址法,该方法不会创建链表,当关键字散列到的数组单元已经被另外一个关键字占用的时候,就会尝试在数组中寻找其他的单元,直到找到一个空的单元。探测数组空单元的方式有很多,这里介绍一种最简单的 – 线性探测法。线性探测法就是从冲突的数组单元开始,依次往后搜索空单元,如果到数组尾部,再从头开始搜索(环形查找)。

ps:ThreadLocalMap中就是使用的开放地址法来解决Hash冲突的,是因为在 ThreadLocalMap 中的散列值分散的十分均匀,很少会出现冲突。并且 ThreadLocalMap 经常需要清除无用的对象,使用纯数组更加方便。

ThreadLocal的使用场景

ThreadLocal 不是用来解决共享对象的多线程访问问题的,数据实质上是放在每个thread实例引用的threadLocalMap,也就是说每个不同的线程都拥有专属于自己的数据容器(threadLocalMap),彼此不影响。因此threadLocal只适用于 共享对象会造成线程安全 的业务场景。比如hibernate中通过threadLocal管理Session就是一个典型的案例,不同的请求线程(用户)拥有自己的session,若将session共享出去被多线程访问,必然会带来线程安全问题。

threadLocal有可能存在内存泄漏,在使用完之后,最好使用remove方法将这个变量移除,就像在使用数据库连接一样,及时关闭连接。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值