自学内容网 自学内容网

ThreadLocal 源码分析

目录

说明

简明

设计思路

使用场景

数据持有

事务管理

深度源码分析

set(T value)

T get()

isPresent()

remove()

过期条目清理

ThreadLocalMap中的replaceStaleEntry方法


说明

        出自本人于2024年10月2日的一场字节的面试, 因为问到了ThreadLocal源码, 没太了解, 不太服气, 才有的此篇章. 

简明

        ThreadLocal相比大家都不陌生, 它是线程私有的, 也就是说, 不同的线程对同一个ThreadLocal变量调用API是不会影响其他线程的, 因为此特性被广泛应用在java多线程并发领域, 有着并发界扛把子, Java面试中流砥柱的地位,  有很多人对它的底层原理望而生畏, 查找了很多视频, 看了五六个小时, 也没什么收获, 我只能说, 不如自己去看看代码是怎么实现的, ThreadLocal类是纯java语言实现的, 而且API也很简单, 结构也不复杂, 你只需要了解哈希这种数据结构即可. 

        下面是ThreadLocal的使用案例, 如下: 

public class Test{
    private static ThreadLocal<Integer> local = new ThreadLocal(){
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
        // 创建一个线程去使用ThreadLocal变量
        Thread t = new Thread(() -> {
            local.set(1);
            System.out.println("线程t的执行结果: " + local.get());
        });
        t.start();

        // 在main线程中设置ThreadLocal变量
        local.set(2);
        System.out.println("main线程的执行结果: " + local.get());
    }
}

运行结果如下: 

main线程的执行结果: 2
线程t的执行结果: 1

进程已结束,退出代码为 0

        可以看到他们之间的使用并不影响. 体现了其线程私有的特性

设计思路

        下面是相关的一个图, 可以参考: 

         我们从上图进行分析

        首先每一个线程都有自己的ThreadLocalMap(key 为 ThreadLocal变量的hash值与它自己的ThreadLocalMap的长度减一进行&操作), 每个线程的代码里面, 如果有使用到ThreadLocal的时候, 都会去自己的Map里面查找, 具体如何查找? 

        首先一个线程对一个ThreadLocal变量进行set操作的时候, set的参数是T value, 是一个泛型参数, 传入了这个参数之后, 首选去拿到这个线程自己的ThreadLocalMap, 具体如何拿, 就是使用: 

Thread t = Thread.currentThread();

        然后通过计算出其 key值, 然后进行散列, 散列使用的是开放地址法来解决hash冲突. 

         如果同一key查询到多个key, 那么就对比其ThreadLocal的实例地址, 然后返回其存储的数据. 

使用场景

        就这么一个特性, 到底能有什么用? 接下来就跟大家探讨一下. 

数据持有

        一种较为常见的场景就是使用ThreadLocal来存储用户常用信息, 例如在Spring的web开发中, 可以通过ThreadLocal在不同的层级之间使用ThreadLocal用户信息, 可以很方便的让用户信息在不同的应用层之间进行传递. 并且不同的线程只会拿到自己的用户信息, 不会出现数据持的一致性问题这也被称为会话管理.

事务管理

        在事务管理中, 一般都遵循着一个事务的几个部分需在同一个线程内运行, 在事务开始的时候去获取JDBC连接, 这个连接也应该保证, 在事务执行的过程中, 始终是同一个连接来处理这个请求, 不然如果有两个连接来处理的话, 就会缺失事务的状态而导致事务执行错误, 从而产生数据不一致性问题.  

        期间, 为了保证不同的Thread拿到的是不同的连接, 并且互不干扰, 于是就可以使用THreadLocal去存储这个连接, 如下: 

        还有很多场景, 可以阅读更加专业的书籍

深度源码分析

        下面有一个代码案例: 

public class Main {
    public static ThreadLocal<String> currentUserName = new ThreadLocal<>();

    public static void main(String[] args) {
        currentUserName.set("张三");
        System.out.println("main中的currentUserName为: " + currentUserName.get());

        new Thread(() -> {
            currentUserName.set("李四");
            System.out.println("线程" + Thread.currentThread().getName() + "中的currentUserName为: " + currentUserName.get());
        }).start();
    }
}

 执行结果如下: 

main中的currentUserName为: 张三
线程Thread-0中的currentUserName为: 李四

进程已结束,退出代码为 0

        可以看出了不同的线程互不干扰, 我们首先来看set方法是如何工作的: 

set(T value)

        set的源码如下: 

    public void set(T value) {
        // 当前线程对象
        Thread t = Thread.currentThread();
        // 获取当前线程对象的ThreadLocalMap 
        ThreadLocalMap map = getMap(t);
        // 如果不为null, 就将当前的Threadlocal作为key, 参数中的value作为value传递给ThreadLocalMap 的set方法.
        if (map != null) {
            map.set(this, value);
        } else {
            // 如果当前线程没有Map, 那么就创建一个, 并初始化
            createMap(t, value);
        }
    }

        从上述代码中可以看出来, 调用一个THreadLocal变量的set方法并传入一个参数的时候, 它会首先去获取当前的线程, 然后将当前线程的引用实例对象的内存地址赋值给getMap(Thread t) 这个方法, 尝试获取到这个线程的ThreadLocalMap, 然后调用map 的set方法, 貌似是将当前的Threadlocal变量作为key, value(参数)作为value

        但是如果使用getMap获取不到当前线程的ThreadLocalMap, 就创建一个, 创建的方法如下: 

    // 为线程t创建ThreadLocalMap实例对象
    void createMap(Thread t, T firstValue) {
        // t.threadLocals 其实就是指的线程t中的ThreadLocalMap实例变量
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

        其实也就是将当前的线程t中的ThreadLocalMap创建实例对象, 并且没有使用默认的构造方法, 而是使用的带参数的构造方法, 来看看这个构造方法: 

        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            // 初始化 table表的容量
            table = new Entry[INITIAL_CAPACITY];
            // 计算hash值
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            // 赋值
            table[i] = new Entry(firstKey, firstValue);
            // ThreadLocal的变量数量置为1
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

        可以看出这其实是一个ThreadLocalMap中的构造方法, 主要是给ThreadLocalMap中的table实例变量初始化了容量, 值为INITIAL_CAPACITY :  16

private static final int INITIAL_CAPACITY = 16;

        此处的table, 其实就是ThreadLocalMap中用来处理ThreadLocal变量和值的映射关系的一个表: 

        private Entry[] table;

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

可以用下图来解释: 

        然后使用当前传进来的Threadlocal的哈希值和容量的-1进行&操作, 得出一个i值, 然后将这个table[i] 赋值给 新的双列元素Entry(firstKey, firstValue). 

        并且将size 置为了1, 因为这是创建ThreadLocalMap的过程, 只有这一个变量需要创建, 然后就调用setThreshold方法, 如下: 

        private void setThreshold(int len) {
            // The next size value at which to resize.
            threshold = len * 2 / 3;
        }

        也就是将threshold的值置为了长度的三分之二.  官方的解释是, 超过了容量的这个值, 就需要重新hash. 

        此时结构就已经很清楚了, 我们再梳理一下里面的结构, 已经流程:

        我来总结一下 , 其实就是从调用Threadlocal变量的set方法开始, 需要经历如下流程: 

         

        这些都很好理解, 剩下的就是拿到了Thread的ThreadLocalMap变量了, 该如何将ThreadLocal和其值映射进去, 调用的 ThreadLocalMap的set方法如下: 

        private void set(ThreadLocal<?> key, Object value) {
            // 获取table的实例地址
            Entry[] tab = table;
            // 计算出当前的容量
            int len = tab.length;
            // 然后根据ThreadLocal的hashcode和容量, 通过下面的算法来计算出hash值(i).
            int i = key.threadLocalHashCode & (len-1);

            // 采用开放地址法解决hash冲突. 
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                // 如果计算出来的hash值, 可以get到对应的ThreadLocal, 
                // 就判断当前的ThreadLocal的地址和get的ThreadLocal的地址是否相等, 
                // 如果相等, 就可以直接替换并返回
                if (k == key) {
                    e.value = value;
                    return;
                }
                // 如果通过hash值拿到了这个Entry, 但是Entry的key为null, 就替换为当前的ThreadLocal地址. 
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            // 如果一开始计算的hash值进行获取的entry为null的时候, 就可以直接在当前的hash值上设置k-v
            tab[i] = new Entry(key, value);
            // 将size+1, 然后将size+1的结果赋值给sz, 并判断是否需要重新hash.
            int sz = ++size;  
            // 判断是否需要重新hash, 其逻辑是sz 必须大于容量的三分之二(前面提到过)
            // 并且必须满足函数cleanSomeSlots方法的返回值为false.
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

T get()

        说完set我们来说get, 这个get是一个获取的流程, 相对来说比较简单: 

    public T get() {
        // 获取当前的线程
        Thread t = Thread.currentThread();
        // 获取当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 通过当前ThreadLocal的this引用查询这个map中的Entry对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                // 返回value
                return result;
            }
        }
        // 构建一个map, 设置初始值并返回
        return setInitialValue();
    }

         也是首先拿到当前的线程, 然后去查找这个线程的ThreadLocalMap变量, 如果这个ThreadLocalMap不为null的话, 就通过getEntry方法, 将ThreadLocal的this作为key传递进去来拿到对应的Entry, 如果这个entry不为null, 就返回器value. 

        否则的话, 就返回setInitialValue()方法中的值: 

        从简单的开始, 我们先看如果使用ThreadLocal引用查不到数据返回setInitialValue()的返回值的情况: 

    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        return value;
    }

        可以看出这个方法, 首先调用了initialValue拿到了一个值value, 如下: 

    protected T initialValue() {
        return null;
    }

        也就是说ThreadLocal被线程调用get, 如果没有显式的set或者重写这个initialValue()方法, 那么默认的返回值其实就为null. 

         然后就是拿到当前线程并尝试获取他的ThreadLocalMap, 如果获取不到, 就创建, 否则就set进去初始值, 并将其返回. 这个set方法与上述的 讲解的一样, 同下面的几个标题. 

        createMap就是为当前的线程创建ThreadLocalMap, 如下: 

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

         创建的逻辑在set中已经讲解. 

        如果当前的ThreadLocalMap不为null, 就去获取Entry : 

        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);
        }

        前面的几步都好说, 分别是计算Hash值, 并且拿到对应Hash值的Entry, 如果这个entry不为null并且key == 当前的ThreadLocal的引用, 那么就会返回这个entry, 否则继续往后根据线性探测法进行搜索. : 

        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    // 线性探测法查找下一个entry的index
                    i = nextIndex(i, len);
                e = tab[i];
            }
            // 此处说明这个hash值没有对应的value, 直接返回null
            return null;
        }

isPresent()

        此方法的源码如下: 

boolean isPresent() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    return map != null && map.getEntry(this) != null;
}

        该方法现在很好理解了, 他现在返回的是. 当前的线程拥有自己的ThreadLocalMap, 并且某个线程在调用这个ThreadLocal的方法的时候, 它必须在这个线程的ThreadLocalMap中有对应的值, 参会返回true, 否则返回false. 



remove()

        开局的源码很简单, 就是通过当前的线程去拿到它的ThreadLocalMap, 如果拿到了, 就执行ThreadLocalMap的remove操作, 将当前的ThreadLocal变量关联的k-v从ThreadLocalMap的table中删除, 并进行过期条目的清理.

    public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null) {
             m.remove(this);
         }
     }

        下面是ThreadLocalMap的remove方法: 

        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

        这段代码上来也是记录了三样东西, 一个是table, 一个是当前table的最大容量, 一个是当前THreadLocal引用在此ThreadLocalMap中的Hash值, 然后去遍历table, 使用线性探测法进行遍历, 如果找到一个key == 当前ThreadLocal引用地址的entry, 那么就调用这个entry的clear()方法将自己清除, 如下: 

    public void clear() {
        this.referent = null;
    }

         但是它只是将自己(entry)的ThreadLocal引用值为了null, 并没有把这个entry本身值为null, 然后再将其下标交给expungeStaleEntry方法来进行过期条目的清理和重哈希, 然后返回.



这个逻辑想必不难, 其中比较难理解的可能是如下这两个方法: 

过期条目清理

  • replaceStaleEntry, 其实看中文名也很好理解, stale其实就是过期, 不新鲜的意思, replace就是替换
  • cleanSomeSlots, 顾名思义, 清理插槽

        居然是清理, 那我们就应该知道, 为什么要清理, 什么样的entry需要被清理, 从代码中不难看出:  要清理的对象一般是entry不为null, 但是entry的ThreadLocal值为null, 就需要清理. 

        那么什么是Entry呢? 如下: 

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        其实意思就是, 我有一个ThreadLocal变量构成的一个Entry, 也就是正在一个线程的THreadlocalMap中作为 table的一个entry, 但是有一天这个ThreadLocal变量突然不使用了被置为了null, 此时这个table中的entry不为null, 但是其key(ThreadLocal地址) 已经被置为null了.  

        它是ThreadlocalMap中的静态内部类, 继承了一个WeakReference, 也就是弱引用, 弱引用指向的对象, 是那些非必须存在的类, 活不过下次垃圾回收. 

        它里面有一个属性为Object类型的变量value, 并且还持有对ThreadLocal的弱引用. 可以通过Entry的继承的WeakReference中的get实例方法来获取这个ThreadLocal对象. 

         上图中的方法expungeStaleEntries方法就是rehash方法里面的内容, rehash顾名思义就是重新hash的意思, 重新hash就必然需要清理掉过期的条目(entry). 并且在有必要的时候重新设置大小: 

 private void rehash() {
            expungeStaleEntries();

            // Use lower threshold for doubling to avoid hysteresis
            if (size >= threshold - threshold / 4)
                resize();
        }

         threshold即为重新hash的容量限制. 上代码中的if中的条件是在说明, 如果在清理完成之后, 如果当前的size 大于 threshold的四分之三就重新设置大小, 其实也就是当前size的三分之二 * 四分之三, 也就是当前size的二分之一. 

        resize的方法如下: 

private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            // 新的内存是旧的2倍, 也就是以两倍扩容
            int newLen = oldLen * 2;
            // new 一个新的table
            Entry[] newTab = new Entry[newLen];
            int count = 0;

            // 将就的数据迁移到新的table中
            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                        // 重新计算单个条目的hash值
                        int h = k.threadLocalHashCode & (newLen - 1);
                        // 开放地址法解决hash冲突
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        // 计数++ 
                        count++;
                    }
                }
            }
            // 设置新的 threshold, size和table
            setThreshold(newLen);
            size = count;
            table = newTab;
        }

         其实rehash的逻辑很简单, 就是过滤掉所有的过期条目, 然后遍历所有的条目, 没有过期的就重新hash, 如果遇到冲突, 就是用开放地址法来解决hash冲突. 最后交换table, 设置新的threshold和size. 

        其实我们回过头来看这个清理所有条目这个方法(expungeStaleEntries), 他其实就是遍历所有的条目, 找出过期的条目(entry不为null, 但是其ThreadLocal值已经为null )的下标i , 然后将其传给expungeStaleEntry(int i)方法, 如下: 

private int expungeStaleEntry(int staleSlot) {
            // 当前的ThreadLocalMap的table
            Entry[] tab = table;
            // old table length
            int len = tab.length;

            // 清理 下标为staleSlot条目
            // 将value值为null, 并且将该条目置为null
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            // 当前的条目数 -1
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            // 向后面遍历, 直到遍历到一个 null值.
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                // 对于不为null的Entry

                // 如果这个entry的ThreadLocal变量值为null, 那么就清理这个过期的条目
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    // 如果这个entry的ThreadLocal变量值不为null, 就重新计算它的哈希值.
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

        大家可能从这个方法就开始看不懂了, 这个地方有一个小细节, 需要大家注意.  首先这个方法会清除staleSlot下标所指向的Entry(将其value值为null之后, 再将Entry本身值为null), 然后将当前的size - 1,  随后的一段代码, 可谓是难倒了很大一部分人, 如下: 

            Entry e;
            int i;
            // staleSlot为需要被清理的过期条目的下标, 根据这个下标, 使用开放地址法获取下一个非null Entry的地址
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                // 如果这个entry的ThreadLocal值为null, 就将其作为过期条目清理(同上)
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    // 如果它的ThreadLocal值不为null, 就需要重新进行hasj
                    int h = k.threadLocalHashCode & (len - 1);
                    // 如果重新计算的hash值与原来的这个Entry里面的ThreadLocal计算的hash值不等的话, 就需要重新进行设置
                    if (h != i) {
                        // 首先将原来的i值下标对应的entry值为null, 方便GC
                        tab[i] = null;
                        // 如果计算出来的新的hash值有冲突, 使用开放地址法解决hash冲突.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }

        根据注释其实不难理解, 为什么我已经清理了当前staleSlot下标对应entry, 还要继续往后面重新哈希所有的非过期条目, 直到下一个null值.  这涉及到hash的开放地址法的删除和插入的问题, 如下, 加入你有四个key-value, 他们四个key相同但是value不同, 这个时候你将他们放到hash表中就会产生hash冲突, 此时如果采用线性探测法, 是一种非常便捷的解决方案, 但是这会产生一种问题

        在存放第二个value到hash表中的时候, 其key跟第一个key发生了一个hash冲突, 此后的hash, 都产生了hash冲突, 如下: 

        所以他才需要才删除了value2之后, 继续计算后面的value3和value4的hash值然后让他们重新hash, 就会变成下面的这个样子: 

        因此这段代码中的: 

            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    // 如果重新计算的hash值与原来的这个Entry里面的ThreadLocal计算的hash值不等的话, 就需要重新进行设置
                    if (h != i) {
                        // 首先将原来的i值下标对应的entry值为null, 方便GC
                        tab[i] = null;
                        // 如果计算出来的新的hash值有冲突, 使用开放地址法解决hash冲突.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }

        里面的else就是在重新hash的过程. 

ThreadLocalMap中的replaceStaleEntry方法

        有了这个基础, 我们再来看看set中调用的replaceStaleEntry方法, 有什么难理解的地方

       看看replaceStaleEntry这个方法, 如下: 

        此方法是在调用set方法的时候以此拿到所有的从当前计算出来的hash值开始, 后面的所有的非nullEntry, 然后检测其Entry的key是否等于当前的set参数中的key, 如果相等就之间替换然后返回, 如果获取的是一个非 null的Entry, 但是是一个过期的条目(Entry.get() 为null, 也就是它里面的ThreadLocal的变量引用为null), 就进行替换.  如下方法就是其替换的方法replaceStaleEntry

其参数解释如下: 

  • key, 为线程调用ThreadLocal的set的时候, 传递进来的this:

  •  value就是线程调用ThreadLocal的时候传递进来的value
  • staleSlot就是需要被替换的Entry(Entry != null 但是 entry.get()为null )的下标. 

        在进来此方法之前的ThreadLocalMap的set方法中, 如果从计算出来的Hash值开始往后面遍历, 如果找到了一个可以entry不为null, 但是ThreadLocal变量为null的就会进入下面的replaceStaleEntry方法, 如下是ThreadLocalMap的set的图解: 

        在到达k == null就会进入到下面的代码 

      // 此处的key参数是Thread的代码中调用ThreadLocal变量的set方法的那个ThreadLocal变量,
        // value 就是要set的值
        // staleSlot 为传进来的i值(也就是通过i get到的entry的key为null的i值)
      private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            int slotToExpunge = staleSlot;  // i值

            // prevIndex用于获取当前i值的前一个hash值, 
            // 例如如果i=2的话, 那么这个方法的返回值就为1
            // 下面这个循环一直往前面扫描, 扫描出来的entry不为null的, 就进行特定的处理
            // 直到找出了一个tab[i]的值, 结束循环
            for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
            // 此处的get方法获取的是当前entry的弱引用, 如果为null, 就记录当前的e的索引
                if (e.get() == null)
                    slotToExpunge = i;

            // Find either the key or trailing null slot of run, whichever
            // occurs first
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;

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

                    // Start expunge at preceding stale entry if it exists
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // If key not found, put new entry in stale slot
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

        这段代码一进来就为我们罗列了一些信息: 

Entry[] tab = table;
int len = tab.length;
Entry e;

        随后的就是一个独立的循环, 我们看看它在做什么: 

int slotToExpunge = staleSlot; // 将传进来的entry不为null,但是其ThreadLocal值为null的entry下标
for (int i = prevIndex(staleSlot, len); // 从这个下标开始往前面遍历不为null的entry对象
     (e = tab[i]) != null;
     i = prevIndex(i, len))
    if (e.get() == null)
        slotToExpunge = i;  // 如果找到了不为null, 但是其ThreadLocal变量为null的, 就将slotToExpunge 更新为这个下标

        随后就是从这个staleSlot往前遍历, 与往后遍历

  • 往前遍历是为了找到能清理的过期的条目的下标
  • 往右查找如果找到了当前ThreadLocal的引用值等于 遍历得到的ThreadLocal的引用值, 就可以进行直接的替换, 如上图entry7的ThreadLocal引用变量的值 == key的值. 让他们两个进行交换, 就变成了下面这个样子: 

         此时slotToExpunge的值变为了当前的i值, 也就是entry不为null, 但是Threadlocal的ref为null的entry值, 此时将其扔个expungeStaleEntry方法(在过期条目清理这个章节中讲到过), 来删除slotToExpunge下标的entry对象. 

        其外层还调用了cleanSomeSlots方法, 参数是上面expungeStaleEntry的返回值和当前table的容量. 

        private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

        字如其名, cleanSomeSLots, 意为清理某些槽, 这个槽是指的什么? 我们来逐行分析代码.. 

        我们先看看方法的参数: 

  • i 为外层的expungeStaleEntry传递进来的,  意思为在进行过期泪目清理重新hash的时候, 返回的最后一个不为null 的entry的下一个null的下标. 

  • len: 该ThreadLocalMap的最大容量 

        首先进来定义了几个变量, 如下: 

boolean removed = false; // 一个boolean字段
Entry[] tab = table; // ThreadLocalMap的Entry[] table表
int len = tab.length; // 该table表的最大容量. 

         下面是一个do while循环, : 

do {
    i = nextIndex(i, len);
    Entry e = tab[i];
    if (e != null && e.get() == null) {
        n = len;
        removed = true;
        i = expungeStaleEntry(i);
    }
} while ( (n >>>= 1) != 0);
return removed;

        首先这个代码先拿到了重新hash的最后一个为null的下一位, 然后去拿到它的Entry, 如果这个Entry不为null, 并且它的ThreadLocal的ref为null, 那么就将其n设置为当前表的table, 并把removed置为true, 表名这个方法发现并且清楚了一个过期条目, 并将这个i继续传入expungeStaleEntry进行清理过期条目和重新hash.  

        剩下的当你一进来这个向右遍历的循环, 的时候, 如果刚开始entry就为null, 那么就会直接进行赋值操作.

        并且如果lotToExpunge != staleSlot, 就说明存在过期条目需要清楚.  


原文地址:https://blog.csdn.net/niceffking/article/details/142687990

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!