ThreadLocal源码分析

一、ThreadLocal简介:

          在threadlocal类的注释上,对threadlocal的介绍如下:

ThreadLocal类提供了线程局部变量,这些变量与普通变量不同,每个线程都可以通过get或set方法来访问自己的独立初始化的变量副本。ThreadLocal实例通常是类中的private static字段,他们希望将状态与某一个线程(例如,用户ID或事物ID)相关联。

二、 核心点:

1. ThreadLocalMap是Thread级别的变量,ThreadLocalMap是ThreadLocal维护的一个内部类,ThreadLocal通过Thread来获取唯一的ThreadLocalMap,获取之后对ThreadLocalMap进行操作;

2. ThreadLocalMap中的Entry[]数组存储数据,初始长度是16,以后每次扩容都是2倍扩容;

3. Entry的key是对threadLocal的弱引用,存储结构如下,每个线程在向ThreadLocal里塞值的时候,其实都是向自己所持有的ThreadLocalMap里面塞值;读取值的时候也是同理,首先从自己线程中取出自己所持有的ThreadLocalMap,然后再根据ThreadLocal引用作为key,取出value。这也就是ThreadLocal实现变量的线程隔离的原因。

三、为什么要用弱引用:

     ThreadLocal中的嵌套内部类ThreadLocalMap,这个类本质上是一个map,和hashMap之类的实现相似,是key-value形式,其中有一个内部类Entry,其中key可以看做是ThreadLocal实例,但是本质是持有ThreadLocal实例的弱引用。使用弱引用的原因是可以将ThreadLocal对象的生命周期和线程的生命周期解绑,持有对ThreadLocal的弱引用,可以使得ThreadLocal在没有其他强引用的时候被回收掉,这样可以避免因为线程得不到销毁导致ThreadLocal对象无法被回收。

假设使用强引用,当ThreadLocal不再使用的时候,发现某个线程中ThreadLocalMap存在对ThreadLocal的强引用,也就是上图中的虚线,那么这个ThreadLocal对象就会因为和Entry对象存在强引用无法被回收,造成内存泄漏。除非线程结束后被回收。

另外一种情况的内存泄漏:

当把threadlocal值为null后,没有任何强引用指向内存中threadlocal实例,threadlocal中的key是它的弱引用,所以会被GC回收,但是线程中threadlocals变量的entry对应的value没有被回收,因为存在着从当前线程对象连接过来的强引用,并且无法通过threadlocal对象的get方法获取到这个value,这个值永远都不会被访问到,所以会存在内存泄漏。只有在当前线程结束后,线程对象的引用不在存在于栈中,强引用断开,内存中的threadlocalMap,value都会被回收。

解决办法:

当线程的某个threadlocal对象使用完后,马上调用remove方法,删除Entry对象;

其实只要线程对象被及时GC,内存泄漏影响不大,只会发生在thrreadlocal对象引用设为null到线程结束那段时间内。如果使用线程池的时候,线程结束不会被销毁,会被服用,这个时候会存在真正的内存泄漏。

补充说明:

java为了最小化内存泄漏的可能性,当调用ThreadLocal的get、set方法的时候,会检查当前key所指向的对象是否为null,如果是则删除对应的value。

四、源码分析:

ThreadLocal类图:

我们着重分析ThreadLocalMap源码。成员变量如下:

    /**
     * 初始容量 —— 必须是2的冥
     */
    private static final int INITIAL_CAPACITY = 16;

    /**
     * 存放数据的table,Entry类的定义在下面分析
     * 同样,数组长度必须是2的冥。
     */
    private Entry[] table;

    /**
     * 数组里面entrys的个数,可以用于判断table当前使用量是否超过负因子。
     */
    private int size = 0;

    /**
     * 进行扩容的阈值,表使用量大于它的时候进行扩容。
     */
    private int threshold; // Default to 0
    
    /**
     * 定义为长度的2/3
     */
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }

ThreadLocal的set方法和Hash映射

threadLocal的set方法源码:

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

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
    }

步骤:

1. 获取当前线程,根据当前线程获取threadLocalMap实例;

2. 如果获取到的map实例不为空,则调用set方法,给map里面设置值,key为threadLocal对象实例,value为要设置的值。如果map为空,则调用构造函数实例化map

从上面可以看出线程中的ThreadLocalMap使用的是延迟初始化,在第一次调用get或者set方法的时候才会初始化。

ThreadLocalMap构造函数:

/**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            //初始化table
            table = new Entry[INITIAL_CAPACITY];
            //计算索引
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            //设置阈值
            setThreshold(INITIAL_CAPACITY);
        }

  主要说一下计算索引的过程,firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)

  •  关于& (INITIAL_CAPACITY - 1),这是取模的一种方式,对于2的幂作为模数取模,用此代替%(2^n),这也就是为啥容量必须为2的冥
  • 关于firstKey.threadLocalHashCode:
    private final int threadLocalHashCode = nextHashCode();
    
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    private static AtomicInteger nextHashCode =
            new AtomicInteger();
            
    private static final int HASH_INCREMENT = 0x61c88647;
    

定义了一个AtomicInteger类型,每次获取当前值并加上HASH_INCREMENT,HASH_INCREMENT = 0x61c88647,关于这个值和斐波那契额散列有关,其原理这里不再深究,感兴趣可自行搜索,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里, 也就是Entry[] table中。

ThreadLocalMap中的set()

ThreadLocalMap使用线性探测法来解决hash冲突,我们都知道hashMap采用的是链地址法解决hash冲突的。线性探测法的地址增量di = 1, 2, ... , m-1,其中,i为探测次数。该方法一次探测下一个地址,知道有地空的地址后插入,如果整个空间都找不到空余的地址,就会产生溢出。可以把table看成一个环形数组,假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。

ThreadLocalMap的set()方法代码如下:

    private void set(ThreadLocal<?> key, Object value) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        //计算索引,上面已经有说过。
        int i = key.threadLocalHashCode & (len-1);

        /**
         * 根据获取到的索引进行循环,如果当前索引上的table[i]不为空,在没有return的情况下,
         * 就使用nextIndex()获取下一个(上面提到到线性探测法)。
         */
        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
            //table[i]上key不为空,并且和当前key相同,更新value
            if (k == key) {
                e.value = value;
                return;
            }
            /**
             * table[i]上的key为空,说明被回收了(上面的弱引用中提到过)。
             * 这个时候说明改table[i]可以重新使用,用新的key-value将其替换,并删除其他无效的entry
             */
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }

        //找到为空的插入位置,插入值,在为空的位置插入需要对size进行加1操作
        tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
        int sz = ++size;

        /**
         * cleanSomeSlots用于清除那些e.get()==null,也就是table[index] != null && table[index].get()==null
         * 之前提到过,这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null。
         * 如果没有清除任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行rehash()
         */
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }


    /**
     * 替换无效entry
     */
    private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                   int staleSlot) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        ThreadLocal.ThreadLocalMap.Entry e;

        /**
         * 根据传入的无效entry的位置(staleSlot),向前扫描
         * 一段连续的entry(这里的连续是指一段相邻的entry并且table[i] != null),
         * 直到找到一个无效entry,或者扫描完也没找到
         */
        int slotToExpunge = staleSlot;//之后用于清理的起点
        for (int i = prevIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = prevIndex(i, len))
            if (e.get() == null)
                slotToExpunge = i;

        /**
         * 向后扫描一段连续的entry
         */
        for (int i = nextIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();

            /**
             * 如果找到了key,将其与传入的无效entry替换,也就是与table[staleSlot]进行替换
             */
            if (k == key) {
                e.value = value;

                tab[i] = tab[staleSlot];
                tab[staleSlot] = e;

                //如果向前查找没有找到无效entry,则更新slotToExpunge为当前值i
                if (slotToExpunge == staleSlot)
                    slotToExpunge = i;
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                return;
            }

            /**
             * 如果向前查找没有找到无效entry,并且当前向后扫描的entry无效,则更新slotToExpunge为当前值i
             */
            if (k == null && slotToExpunge == staleSlot)
                slotToExpunge = i;
        }

        /**
         * 如果没有找到key,也就是说key之前不存在table中
         * 就直接最开始的无效entry——tab[staleSlot]上直接新增即可
         */
        tab[staleSlot].value = null;
        tab[staleSlot] = new ThreadLocal.ThreadLocalMap.Entry(key, value);

        /**
         * slotToExpunge != staleSlot,说明存在其他的无效entry需要进行清理。
         */
        if (slotToExpunge != staleSlot)
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    }

    /**
     * 连续段清除
     * 根据传入的staleSlot,清理对应的无效entry——table[staleSlot],
     * 并且根据当前传入的staleSlot,向后扫描一段连续的entry(这里的连续是指一段相邻的entry并且table[i] != null),
     * 对可能存在hash冲突的entry进行rehash,并且清理遇到的无效entry.
     *
     * @param staleSlot key为null,需要无效entry所在的table中的索引
     * @return 返回下一个为空的solt的索引。
     */
    private int expungeStaleEntry(int staleSlot) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;

        // 清理无效entry,置空
        tab[staleSlot].value = null;
        tab[staleSlot] = null;
        //size减1,置空后table的被使用量减1
        size--;

        ThreadLocal.ThreadLocalMap.Entry e;
        int i;
        /**
         * 从staleSlot开始向后扫描一段连续的entry
         */
        for (i = nextIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();
            //如果遇到key为null,表示无效entry,进行清理.
            if (k == null) {
                e.value = null;
                tab[i] = null;
                size--;
            } else {
                //如果key不为null,计算索引
                int h = k.threadLocalHashCode & (len - 1);
                /**
                 * 计算出来的索引——h,与其现在所在位置的索引——i不一致,置空当前的table[i]
                 * 从h开始向后线性探测到第一个空的slot,把当前的entry挪过去。
                 */
                if (h != i) {
                    tab[i] = null;
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    tab[h] = e;
                }
            }
        }
        //下一个为空的solt的索引。
        return i;
    }

    /**
     * 启发式的扫描清除,扫描次数由传入的参数n决定
     *
     * @param i 从i向后开始扫描(不包括i,因为索引为i的Slot肯定为null)
     *
     * @param n 控制扫描次数,正常情况下为 log2(n) ,
     * 如果找到了无效entry,会将n重置为table的长度len,进行段清除。
     *
     * map.set()点用的时候传入的是元素个数,replaceStaleEntry()调用的时候传入的是table的长度len
     *
     * @return true if any stale entries have been removed.
     */
    private boolean cleanSomeSlots(int i, int n) {
        boolean removed = false;
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        do {
            i = nextIndex(i, len);
            ThreadLocal.ThreadLocalMap.Entry e = tab[i];
            if (e != null && e.get() == null) {
                //重置n为len
                n = len;
                removed = true;
                //依然调用expungeStaleEntry来进行无效entry的清除
                i = expungeStaleEntry(i);
            }
        } while ( (n >>>= 1) != 0);//无符号的右移动,可以用于控制扫描次数在log2(n)
        return removed;
    }


    private void rehash() {
        //全清理
        expungeStaleEntries();

        /**
         * threshold = 2/3 * len
         * 所以threshold - threshold / 4 = 1en/2
         * 这里主要是因为上面做了一次全清理所以size减小,需要进行判断。
         * 判断的时候把阈值调低了。
         */
        if (size >= threshold - threshold / 4)
            resize();
    }

    /**
     * 扩容,扩大为原来的2倍(这样保证了长度为2的冥)
     */
    private void resize() {
        ThreadLocal.ThreadLocalMap.Entry[] oldTab = table;
        int oldLen = oldTab.length;
        int newLen = oldLen * 2;
        ThreadLocal.ThreadLocalMap.Entry[] newTab = new ThreadLocal.ThreadLocalMap.Entry[newLen];
        int count = 0;

        for (int j = 0; j < oldLen; ++j) {
            ThreadLocal.ThreadLocalMap.Entry e = oldTab[j];
            if (e != null) {
                ThreadLocal<?> k = e.get();
                //虽然做过一次清理,但在扩容的时候可能会又存在key==null的情况。
                if (k == null) {
                    //这里试试将e.value设置为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;
    }

    /**
     * 全清理,清理所有无效entry
     */
    private void expungeStaleEntries() {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        for (int j = 0; j < len; j++) {
            ThreadLocal.ThreadLocalMap.Entry e = tab[j];
            if (e != null && e.get() == null)
                //使用连续段清理
                expungeStaleEntry(j);
        }
    }

ThreadLocalMap中的getEntry()

    private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> key) {
        //根据key计算索引,获取entry
        int i = key.threadLocalHashCode & (table.length - 1);
        ThreadLocal.ThreadLocalMap.Entry e = table[i];
        if (e != null && e.get() == key)
            return e;
        else
            return getEntryAfterMiss(key, i, e);
    }

    /**
     * 通过直接计算出来的key找不到对于的value的时候适用这个方法.
     */
    private ThreadLocal.ThreadLocalMap.Entry getEntryAfterMiss(ThreadLocal<?> key, int i, ThreadLocal.ThreadLocalMap.Entry e) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;

        while (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == key)
                return e;
            if (k == null)
                //清除无效的entry
                expungeStaleEntry(i);
            else
                //基于线性探测法向后扫描
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
    }

ThreadLocalMap的remove()

    private void remove(ThreadLocal<?> key) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        //计算索引
        int i = key.threadLocalHashCode & (len-1);
        //进行线性探测,查找正确的key
        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            if (e.get() == key) {
                //调用weakrefrence的clear()清除引用
                e.clear();
                //连续段清除
                expungeStaleEntry(i);
                return;
            }
        }
    }

五、为什么通常将ThreadLocal设计成private static

为了避免重复创建TSO(thread specific objec,与线程相关的变量)。我们知道,一个threadLocal实例对应当前线程的一个TSO实例,因此把threadLocal声明为某个类的实例变量,那么每创建一个该类的实例就会导致一个新的TSO实例被创建。于是,同一个线程可能访问到同一个TSO的不同实例,这即便不会导致错误,也会造成资源浪费,因此,一般我们都会把threadLocal声明为static。

六、ThreadLocal为什么要用线性探测解决哈希冲突

开放地址法的优缺点:

1. 容易产生堆积问题,不适合做大规模数据存储。

2. 散列函数的设计对冲突会有很大影响,插入后可能会出现多次冲突现象。

3. 如果删除的元素是多个冲突元素中的一个,需要对后面的元素做处理,实现较复杂,且时间复杂度会比较高。

链地址法优缺点:

1. 处理冲突简单,且无堆积现象,平均查找长度短。

2. 链表中的节点时动态申请的,适合构造表不能确定长度的情况

3. 删除节点的操作易于实现。只要简单的删去链表中相应的节点即可。

4. 指针需要额外的空间,故当节点规模较小时,开放地址法较为节省空间。

为什么要用开放地址法的原因:

1. ThreadLocal中的属性HASH_INCREMENT能够让哈希码均匀分布在2的N次方的数组里面。

2. ThreadLocal往往存放的数据量并不大(而且key是弱引用又会被垃圾回收,及时让数据量更小),这个时候开放地址法简单的结构会节省空间,同时数组查询效率更高,加上第一点的保证,冲突概率也低。


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值