Java并发编程之ThreadLocal(三)

43 阅读3分钟

前言

上一篇文章中在介绍set方法的流程时,如果找到的索引位置Entrykeynull,表示该Entry已经过期了,就会调用replaceStaleEntry用新的Entry替换掉旧的,这篇文章就来看看这个方法都做了哪些事。

替换过期Entry

// 后一个索引
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}  
// 前一个索引
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

// 替换过期Entry
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;
    // 清理的位置
    int slotToExpunge = staleSlot;
    // 往前循环找
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;
    // 往后找
    for (int i = nextIndex(, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == key) {  // 找到了key
            e.value = value;    // 更新数据
            tab[i] = tab[staleSlot];  // 和hash定位的索引位置进行交换
            tab[staleSlot] = e;
            if (slotToExpunge == staleSlot)  // staleSlot前一个位置就是null,所以第一个for没有执行
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }
        if (k == null && slotToExpunge == staleSlot)  // 第一个for没执行,更新为后面下一个过期节点位置
            slotToExpunge = i;
    }
    // 没找到key,创建新的Entry
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // 有过期的Entry,执行清理操作
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

首先看一下prevIndexnextIndex方法,其实就是索引减一和加一,只不过到数组边界后又会继续从另一头继续,有点像循环链表。

第一个for循环从当前key的索引位置staleSlot前一个开始往前找,只要该位置有数据,就调用Entry.get()获取存储的key,如果keynull,就修改slotToExpunge为该索引,它的作用就是尽可能找到更多过期(Entrykeynull)的节点。

第二个for循环是从当前key的位置staleSlot的下一个位置开始往后找,如果找到了key,就更新value,然后和keyhash定位的索引位置数据进行互换,这个互换操作可以通俗地理解为:我计算出来数据应该是放在i的位置,但是最后放在了j的位置,把这两个位置的数据交换一下我的数据就在i的位置了,这样下次找的时候就能直接找到,不用再挨个往后遍历了。

slotToExpunge的初始值就是staleSlot,而第一个for循环是从前一个位置开始的,所以如果第一个for循环执行过了slotToExpunge就不可能是staleSlot了,所以if (slotToExpunge == staleSlot) 这个条件成立的情况就是前一个位置就是null,所以第一个for循环就没有执行。上面进行数据交换其实就是对staleSlot和i两个位置交换的数据,所以slotToExpunge要重新执行i的位置。

expungeStaleEntry(slotToExpunge)方法的作用是从slotToExpunge位置开始往后找,只要没遇到null的节点就把所有过期的节点都清除掉(这就理解第一个for循环为啥是往前找了,先往前找到更多的过期节点,然后清除时从该位置往后清),然后把有效的数据重新计算hash,移动到新的位置。(源码分析见下篇文章)

cleanSomeSlots(expungeStaleEntry(slotToExpunge), len)方法的作用是继续清理过期的数据。(源码分析见下篇文章)

if (k == null && slotToExpunge == staleSlot) 
    slotToExpunge = i;

slotToExpunge == staleSlot和前面一样表示还是初始位置没有往前找,如果k == null表示当前位置节点为过期节点,就更新slotToExpungei的位置,也就是往后移了一位。

如果第二个for循环没有找到key,就直接清除掉staleSlot位置的数据,然后创建新的节点把数据存到该位置。然后如果lotToExpunge为位置发生过变化,表示有过期的Entry,就执行清理操作。