乐趣区

关于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);
        }
退出移动版