关于java:简单扣一下ThreadLocal源码

简介

ThreadLocal外部定义了一个动态外部类ThreadLocalMap,这个Map的key就是以后的ThreadLocal,value则是要存储的线程局部变量。精确的说key保留的是ThreadLocal的弱援用。在每个线程外部会保护这样的ThreadLocal.ThreadLocalMap,这样做的益处显而易见,能够做到线程之间隔离,并且随着线程敞开,ThreadLocalMap以及其中的对象值会被回收。

插入数据

获取以后线程的ThreadLocalMap,如果已初始化就直接插入,如果未初始化就先初始化再插入。

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

咱们先来剖析ThreadLocalMapset办法

private void set(ThreadLocal<?> key, Object value) {

            Entry[] tab = table;
            int len = tab.length;
            // 依据Hash值计算数据该插入的地位
            // 这个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雷同做笼罩操作
                if (k == key) {
                    e.value = value;
                    return;
                }
                // key为null,阐明曾经生效,作为弱援用,已被GC
                // 此时须要清理过期Key对应的value
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //走到这里,阐明遇到了一个空的槽,则直接插入
            tab[i] = new Entry(key, value);
            int sz = ++size;
            //先清理过期的槽,并判断是否要扩容
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                // 进行扩容,2倍,扩容前也会清理过期的槽
                rehash();
        }

能够看出在一个简略的set办法中有屡次波及到清理过期的槽,尽管清理的办法有多个,然而最终都是调用expungeStaleEntry办法。入参就是某个过期槽在桶中的地位。

 private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            // 以后过期的槽置为null,不便GC
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            Entry e;
            int i;
            //从以后过期槽的下一个地位开始遍历,直到遇到空槽
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                //这里将过期的槽置为null
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    //留神:走到这个分支,以后的槽未生效,后面过期槽的地位曾经被腾出
                    //获取槽i处结点本来应寄存的地位
                    //因为ThreadLocal解决Hash抵触的形式,h必然小于等于i
                    int h = k.threadLocalHashCode & (len - 1);
                    //h!=i,阐明i处寄存的结点不是它本来地位,须要移动
                    if (h != i) {
                        //将该处的结点挪走,置为null
                        tab[i] = null;
                        // 从它本来应寄存的地位h,开始遍历,
                        // 找到一个最靠近h地位的空槽,并插入数据
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            // 返回的桶中空槽的地位
            return i;
        }

通过下面源码剖析,expungeStaleEntry办法做了两件事:

  • 遍历桶,清理过期的key对应的value和槽(遇到空槽进行)
  • 遇到未生效的槽,尝试将该槽的结点往凑近它本来地位的左近移动
    对于第二点,咱们须要补充一下,ThreadLocal解决Hash抵触的形式:
    在往ThreadLocalMap中set值时,如果以后地位曾经被占,且key不雷同,那它就往后找一个空的槽寄存,也就是说,Map桶中元素的地位可能不是该元素自身该当寄存的地位,而是靠后,所以清理过期槽后,尽量把它挪到凑近本来地位左近,这个是为了查问不便

获取值

其实剖析完了set办法,咱们大抵也能猜出查询方法是怎么回事,无非就是依据key计数出桶的地位,如果该地位的key与以后的key不是同一个,那就往后遍历,找到key值雷同的结点,遍历过程中顺带手清理一下过期的槽。

  public T get() {
        Thread t = Thread.currentThread();
        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;
            }
        }
        //往map中存一个null的Value,并返回
        return setInitialValue();
    }

简略看下map中的get办法

       private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            //查到就返回
            if (e != null && e.get() == key)
                return e;
            else
                //查不到就遍历往后找
                return getEntryAfterMiss(key, i, e);
        }

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理