前言

ThreadLocal 是什么?

ThreadLocal是一个为每个线程创立单独变量副本的类。ThreadLocal主要处理的是让每一个线程绑定自己的值,自己用自己的,不跟别的线程争抢。该类提供了线程部分 (thread-local) 变量。经过运用get()set()办法能够拜访线程自己的、独立于初始化的变量副本。即经过ThreadLocal能够让指定的值完成线程隔离(线程之间不存在同享联系),然后防止了线程安全的问题

ThreadLocal 的常用API

  1. public void set(T value):将当时线程的此线程部分变量的副本设置为指定的值

  2. public T get():回来当时线程的此线程部分变量的副本中的值

  3. public void remove():删除此线程部分变量的当时线程的值

  4. protected T initialValue():回来此线程部分变量的当时线程的初始值

  5. public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier):(JDK1.8后参加的)创立线程部分变量,并经过供给者接口设置此线程部分变量的当时线程的初始值(查看源码能够看到其实最终仍是重写了initialValue()来设置初始值的)

比方

public static void main(String[] args) {
    // JDK1.8前创立办法(经过重写initialValue设置初始值)
    ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>(){
        @Override
        protected Integer initialValue() {
            return 666;
        }
    };
    // JDK1.8后创立办法
    ThreadLocal<Integer> threadLocal2 = ThreadLocal.withInitial(() -> 999);
    // 获取threadLocal1
    System.out.println(threadLocal1.get());
    // 获取threadLocal2
    System.out.println(threadLocal2.get());
    // 设置threadLocal1的值
    threadLocal1.set(6);
    // 获取threadLocal1
    System.out.println(threadLocal1.get());
    // 删除threadLocal1的值
    threadLocal1.remove();
    // 获取threadLocal1
    System.out.println(threadLocal1.get());
}
/*
 测验成果:
    666
    999
    6
    666
*/

假如仔细观察测验成果的话,会发现履行了remove()办法删除值后经过get查看发现成果居然不是null,而是又变为了初始值。其实它真的删了,仅仅后续在调用get()时,底层的履行流程是假如回来不了值那么会调用setInitialValue()从头拿到初始化的值并从头初始化并回来初始化的值。有爱好的能够接着往下看源码剖析。

ThreadLocal 的运用场景举例

这儿举一个Java开发手册中推荐运用的比方:运用ThreadLocal完成SimpleDateFormat线程安全。

深入理解ThreadLocal

首要,SimpleDateFormat是线程不安全的,可是开发中经常需求运用它来格式化时刻,下面经过代码来演示为什么是线程不安全的。

/**
 * 测验
 * @author 爱好使然的L
 */
public class SimpleDateFormatTest {
    // 界说时刻格式
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
    public static void main(String[] args) {
        // 创立多线程测验
        while (true) {
            new Thread(() -> {
                // 记载格式化当时时刻
                String res1 = simpleDateFormat.format(new Date());
                try {
                    // 将前次记载的时刻从头格式化
                    Date date = simpleDateFormat.parse(res1);
                    String res2 = simpleDateFormat.format(date);
                    // 比较两次成果是否相同
                    System.out.println(res1.equals(res2));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
/*
 部分测验成果:
    true
    true
    false
    true
    false
    false
    true
*/

能够看到假如是线程安全的状况下,那么回来成果永远为true。SimpleDateFormat线程不安全大致是当多个线程调用format()时会调用calender.setTime(),会导致time被别的线程修正,所以会导致两次成果不相同。

接下来,经过ThreadLocal进行改进。

/**
 * 测验
 * @author 爱好使然的L
 */
public class SimpleDateFormatTest {
    private static final ThreadLocal<DateFormat> dataFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
    public static void main(String[] args) {
        // 创立多线程测验
        while (true) {
            new Thread(() -> {
                // 记载格式化当时时刻
                String res1 = dataFormat.get().format(new Date());
                try {
                    // 将前次记载的时刻从头格式化
                    Date date = dataFormat.get().parse(res1);
                    String res2 = dataFormat.get().format(date);
                    // 比较两次成果是否相同
                    System.out.println(res1.equals(res2));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
/*
 部分测验成果:
    true
    true
    true
    true
    true
    true
    true
*/

能够看到经过ThreadLocal能够处理SimpleDateFormat线程不安全问题。

运用 ThreadLocal 的机遇

当面对线程不安全的问题时,通常会运用加锁等办法确保线程安全,可是上面的比方并没有运用加锁的办法来防止线程不安全问题,而是运用ThreadLocal的办法。ThreadLocal是经过让每个线程具有自己的变量来完成线程隔离。所以我认为运用ThreadLocal的机遇是当需求处理的线程不安全变量(如SimpleDateFormat)不需求线程之间同享(即不需求每个线程运用同一个变量)时,能够不运用加锁的办法来处理线程不安全问题,而是运用ThreadLocal的办法隔离线程。

当然除了线程安全问题之外,ThreadLocal也能够用作Session办理,或者是保存每个线程都具有自己的一个值,比方前后端经过token进行鉴权操作时,后端需求将前端的token进行保存,此时能够运用ThreadLocal对token进行存储,在调用API前过滤器能够经过ThreadLocal拿到token进行鉴权操作。


ThreadLocal 的常见问题 & 源码剖析

一个小小的 ThreadLocal 其实包含了许多有意思的常识

  • ThreadLocal是怎样完成线程隔离的?
  • ThreadLocal为什么有内存走漏问题?
  • ThreadLocal底层运用了斐波那契散列办法来核算Hash?
  • ThreadLocal运用到了弱引证?
  • ThreadLocal运用了勘探式&启发式整理办法来处理过期的Key?

接下来从 ThreadLocal 的源码动身,渐进式的了解上面的问题。

1. ThreadLocal怎样完成的线程隔离?

Thread & ThreadLocal & ThreadLocalMap 三者美妙的联系

  • Thread类有一个ThreadLocal.ThreadLocalMap类型的变量threadLocals,即每个线程都会有一个自己的ThreadLocalMap实例。

    深入理解ThreadLocal

  • ThreadLocalMapThreadLocal的静态内部类。而且ThreadLocalMap经过本身的内部类Entry来完成数据的存储,能够简单地看成是类似于HashMap的k-v存储办法,其中的key是ThreadLocal类型,value是Object类型

    深入理解ThreadLocal

这三者的联系可能有些紊乱,经过ThreadLocal调用get()的进程图了解一下

深入理解ThreadLocal

怎样完成线程隔离的?

Thread类保护了一个变量名为threadLocals的线程版的Map<ThreadLocal,Object>,当需求运用ThreadLocal类来存储数据时,会先拿到ThreadLocal的实例,并ThreadLocal的实例作为key,将要存储的值作为value存入当时线程的threadLocals的Entry中。当需求取出值时,相同经过ThreadLocal的实例作为key,去当时线程保护的threadLocals中取出。经过这样的办法,能够发现,每个线程都保护着自己的threadLocals,即每个线程都有自己的独立变量,这样子在并发的状况下就不会形成线程不安全的问题,由于不存在变量同享,因而完成了线程隔离。


2. ThreadLocal根本办法的源码剖析

ThreadLocal的 get() 办法履行流程

  1. get()
public T get() {
    // 获取当时线程
    Thread t = Thread.currentThread();
    // 拿到当时线程的threadLocals实例
    ThreadLocalMap map = getMap(t);
    // 判别threadLocals是否初始化
    if (map != null) {
        // 经过this当时ThreadLocal方针实例调用ThreadLocalMap的getEntry()取出成果(后续文章详解)
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 取得出值则回来
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // threadLocals未初始化或threadLocals经过this取出的值为null,则进行初始化
    return setInitialValue();
}
  1. getMap()
// 获取线程的threadLocals
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
  1. setInitialValue()
// 初始化办法
private T setInitialValue() {
    // 获取initialValue()的值(初始值),创立ThreadLocal实例时能够经过重写的办法赋上初始值
    T value = initialValue();
    // 获取当时线程
    Thread t = Thread.currentThread();
    // 获取线程的threadLocals
    ThreadLocalMap map = getMap(t);
    // 假如threadLocals尚未初始化,则履行createMap,否者直接设置值
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
    if (this instanceof TerminatingThreadLocal) {
        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
    }
    // 最终回来从initialValue()获取的初始值
    return value;
}

4.createMap()

// 创立(初始化)ThreadLocalMap,并经过firstValue设置初始值
void createMap(Thread t, T firstValue) {
    // 这儿涉及到ThreadLocalMap后续文章详解
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

总结get()履行流程:

  • 先经过Thread.currentThread()获取当时线程,并经过getMap()获取当时线程的ThreadLocalMap实例threadLocals。
  • 判别获取到的threadLocals是否现已初始化(判别是否为null):
    • threadLocals现已初始化则经过当时ThreadLocal的方针this作为参数(作为ThreadLocalMapEntry的key)调用ThreadLocalMap的getEntry()获取对应的值。
  • 假如 threadLocals未初始化或者经过getEntry(this)获取到的值为空,则调用ThreadLocal的 setInitialValue()对threadLocals初始化
  • setInitialValue()履行进程:
    • setInitialValue()中会调用重写过的initialValue()获取指定初始值(没有重写则为null),再次经过getMap()获取threadLocals方针。
    • 判别threadLocals方针是否为null:已初始化则调用ThreadLocalMap的set(this, value)将当时的ThreadLocal方针作为key设置值value。 未初始化则调用createMap()对threadLocals进行初始化new ThreadLocalMap(this, firstValue))并设置上初始值。

总的来说便是调用get()假如threadLocals存在且能查到值则直接回来,threadLocals不存在或者查不到值(为null)则初始化并回来初始化的值(这个值是经过重写initialValue()赋值的)。


ThreadLocal的 set(T value) 办法履行流程

许多细节当地在get()中提到,这儿就不再进行流程总结

public void set(T value) {
    // 1. 获取当时线程
    Thread t = Thread.currentThread();
    // 2. 获取当时线程的threadLocals方针
    ThreadLocalMap map = getMap(t);
    // 3. 判别threadLocals方针是否初始化
    if (map != null) {
        // 已初始化则直接设置值
        map.set(this, value);
    } else {
        // 未初始化则将当时设置的值作为初始值进行初始化
        createMap(t, value);
    }
}

ThreadLocal的 remove() 办法履行流程

public void remove() {
    // 直接一步到位获取当时线程的threadLocals
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null) {
        // 经过ThreadLocalMap的remove()删除,后续文章详解
        m.remove(this);
    }
}

总结

经过上述的三个ThreadLocal的中心办法能够发现以下几点:

  • ThreadLocal本身并并不存储值,调用ThreadLocalset()办法时,实际上便是往ThreadLocalMap设置值,key是ThreadLocal方针,值Value是传递进来的方针,调用ThreadLocalget()办法时,实际上便是往ThreadLocalMap获取值,key是ThreadLocal方针。
  • 经过new的办法创立ThreadLocal实例时,并没有直接将ThreadLocal实例作为key,初始值作为value去存储进当时线程的threadLocals中,而是在后续调用get()或者set()时才去将当时信息参加Entry中。
  • 一旦调用get()获取值时,只会回来经过set()设置的值或者初始值,不会回来null,除非初始值便是null。

3. ThreadLocal为什么会形成内存走漏?

运用弱引证的ThreadLocalMap

深入理解ThreadLocal

能够看到ThreadLocalMap下的Entry静态内部类的key运用了弱引证。

ThreadLocal的内存走漏问题之一:为什么需求运用弱引证?

深入理解ThreadLocal

经过上图剖析,假定现在创立并运用了ThreadLocal,当运用完后,会毁掉Stack中的ThreadLocal方针引证,可是还存在Entry方针的Key方针引证了ThreadLocal方针,因而假如Key是强引证,那么会导致ThreadLocal方针即便现已运用完了而且Stack中的引证也毁掉了,可是由于还存在Key的引证,所以GC不能收回ThreadLocal方针,所以会形成内存走漏。

所以这便是为什么底层需求Entry的Key需求运用弱引证的原因,运用弱引证,发生GC时,假如方针只被弱引证指向,那么就会被收回。

ThreadLocal的内存走漏问题之二:运用弱引证就能确保100%不会形成内存走漏吗?

仍是经过上图剖析,正常状况下是Key方针运用的弱引证能够确保ThreadLocal方针被GC收回,使得Entry呈现Key为null,Value方针还存在的状况。那么这样的Entry方针怎样被收回呢,ThreadLocalMap具有铲除过期的Key的能力ThreadLocalMap调用get()或者set()办法会查看key为null时会调用整理办法进行整理。具体完成在后续文章的勘探式&启发式整理会阐明。

可是假定当时状况为:运用线程池来办理线程,那么会存在线程复用的状况,那么Thread方针将不会被毁掉,而生产中经常会运用static final修饰声明的ThreadLocal方针来保存一个全局的ThreadLocal方针便利传递其他value,这样会导致ThreadLocal一向被强引证,即便Key是弱引证,但强引证不释放,GC便无法收回,所以这种状况下Key不会为null,一起Value也会有值,那么整理办法也不会整理这样的Entry方针,这样就会导致可能现已不用的ThreadLocal无法被收回,形成内存走漏。

如何防止ThreadLocal的内存走漏问题?

经过上面两个内存走漏问题的剖析可知:假如无法确保线程会结束(比方生产中运用线程池办理线程),一定要记住手动运用remove()办法(底层会将key设置为null,经过这样的办法标识该ThreadLocal方针不再运用,后续会被过期Key整理办法铲除Entry),然后防止内存走漏问题。

Java开发手册中推荐这样运用:

深入理解ThreadLocal


4. ThreadLocalMap的斐波那契散列法

ThreadLocalMap的存储结构

与HashMap不同,ThreadLocalMap采用的是数组结构存储办法,当呈现哈希磕碰时,并不是向HashMap那样存在链表或红黑树中,而是采用敞开寻址的办法进行存储(遇到哈希磕碰时,会下标往后寻觅直到遇到Entry为null时停止寻觅,并放入当时Entry)。

ThreadLocalMap在核算Hash时更重视让数据散列均匀,所以ThreadLocalMap采用斐波那契散列法来核算Hash,这样能比较好让数据更加散列,减少哈希磕碰。经过累加 0x61c88647 更新HashCode的值,这儿的0x61c88647是一个哈希值的黄金分割点。

源码

// 黄金分割点
private static final int HASH_INCREMENT = 0x61c88647;
// 数组长度
private static final int INITIAL_CAPACITY = 15;
// 原子类记载一向在更新的HashCode
private static AtomicInteger nextHashCode = new AtomicInteger();
// 记载本身方针的HashCode
private final int threadLocalHashCode = nextHashCode();
// 更新HashCode(每创立一个ThreadLocal方针则更新一次,一起新方针会经过threadLocalHashCode记载自己的HashCode)
private static int nextHashCode() {
    // 每次更新都是累加HASH_INCREMENT
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// 核算下标(取出本身的threadLocalHashCode)
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

由于ThreadLocal方针以本身为key,所以每个方针都能够经过threadLocalHashCode记载归于自己仅有的HashCode。在经过key查找下标时也只需求取出自己的threadLocalHashCode作为HashCode进行查找即可。

下面模仿源码的办法完成斐波那契散列查看一下散列效果

/**
 * 看看斐波那契散列有多均匀
 * @author 爱好使然的L
 */
public class Test {
    // 黄金分割点
    private static final int HASH_INCREMENT = 0x61c88647;
    // 数组长度
    private static final int INITIAL_CAPACITY = 15;
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            // 获取hashCode
            int hashCode = i * HASH_INCREMENT + HASH_INCREMENT;
            // hashCode & (数组长度 - 1) 得到对应得下标方位
            int idx = hashCode & (INITIAL_CAPACITY - 1);
            System.out.println("第 " + i + " 位散列的成果为 " + idx);
        }
    }
}
/*
  成果:
    第 0 位散列的成果为 7
    第 1 位散列的成果为 14
    第 2 位散列的成果为 5
    第 3 位散列的成果为 12
    第 4 位散列的成果为 3
    第 5 位散列的成果为 10
    第 6 位散列的成果为 1
    第 7 位散列的成果为 8
    第 8 位散列的成果为 15
    第 9 位散列的成果为 6
    第 10 位散列的成果为 13
    第 11 位散列的成果为 4
    第 12 位散列的成果为 11
    第 13 位散列的成果为 2
    第 14 位散列的成果为 9
    第 15 位散列的成果为 0
*/

能够看到测验成果很均匀,这儿核算hashCode时运用的是i * HASH_INCREMENT + HASH_INCREMENT,不同于源码这是由于需求经过参加i核算来作为自己的标识。

关于斐波那契散列法的具体细节这儿不再深化,假如开发中用得到斐波那契散列法能够依照上面模仿的代码办法完成即可。


5. ThreadLocalMap根本办法的源码剖析

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) 源码剖析

// 结构器
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 拓荒长度为INITIAL_CAPACITY的Entry数组
    table = new Entry[INITIAL_CAPACITY];
    // 经过firstKey本身的Hashcode核算出下标
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // 存入下标对应的Entry中
    table[i] = new Entry(firstKey, firstValue);
    // 更新size
    size = 1;
    // 设置扩容阈值为传入参数的2/3
    setThreshold(INITIAL_CAPACITY);
}

set(ThreadLocal<?> key, Object value) 源码剖析

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    // 核算key的下标
    int i = key.threadLocalHashCode & (len-1);
    // 敞开寻址办法向后遍历
    for (Entry e = tab[i];
         e != null;  // 直到找到Entry为null
         e = tab[i = nextIndex(i, len)]) {  // nextIndex环状向后遍历(不存在越界问题,到结尾为设置为最初)
        // 获取当时Entry的key
        ThreadLocal<?> k = e.get();
        // 状况一:当时key和需求设置的key相同,直接更新值
        if (k == key) {
            e.value = value;
            return;
        }
        // 状况二:遇到过期的key,进行替换过期数据操作
        if (k == null) {
            // 替换过期数据,底层调用了启发式整理和勘探式整理
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 状况三:找到空的Entry,直接参加并更新size
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 最终调用一次启发式整理办法,整理过期的Key。整理完成后假如size仍是超过了扩容阈值则进行rehash操作
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
}

set()流程概述

  • 经过参数key(方针key)核算出数组的下标方位,从当时下标方位向后遍历Entry数组。
  • 状况一:假如当时遍历的方位的Entry为空,则直接跳出循环,将需求设置的数据存储在当时Entry中。
  • 状况二:假如当时遍历的方位的Entry不为空而且该Entry的key与方针key共同,则直接将需求设置的value掩盖掉该Entry的value即可(掩盖操作)。
  • 状况三:假如当时遍历的方位的Entry不为空而且该Entry的key为null,阐明当时的Entry是过期数据,此时履行replaceStaleEntry()替换过期数据(替换操作),具体操作后边解说。
  • 循环结束后,调用cleanSomeSlots()做一次启发式整理,整理数组中过期的数据。
  • 假如整理后的Entry数量sz仍是大于扩容阈值时则 履行rehash() 进行勘探式整理以及判别是否需求扩容。(rehash()后续机制中具体解说)。

弥补一下replaceStaleEntry() 源码以及概述

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;
    int slotToExpunge = staleSlot;
    // 更新slotToExpunge确保其是最前的过期数据下标
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;
    // 向后遍历看看是否有共同的key能够掩盖数据
    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;
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);
    // 最终进行勘探式和启发式整理
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

replaceStaleEntry()履行流程概述

  • 开端时会创立一个 slotToExpunge 变量用于记载最前的过期数据下标,这个变量后续会用于在勘探式整理时当作参数传入
  • 创立时 slotToExpunge = staleSlotstaleSlotreplaceStaleEntry传入的参数表明当时查看出的过期的Entry下标。
  • 经过从staleSlot下标处向前遍历Entry数组,更新slotToExpunge 确保其是最前的过期数据下标。
  • 经过从staleSlot下标处向后遍历Entry数组,假如遇到了当时遍历的Entry的key与方针key共同时,会进行三步处理,履行完后直接回来。
    • ① 直接掩盖value。
    • ② 将当时的Entry与staleSlot下标处的Entry进行交换,把过期数据移到后边。
    • ③ 判别一下slotToExpunge == staleSlot,阐明replaceStaleEntry()一开端向前查找过期数据时并未找到过期的Entry数据,接着向后查找进程中也未发现过期数据,修正slotToExpunge = i指直接从当时方位开端履行勘探式整理就好了,而且将勘探式整理回来的值当作参数传给cleanSomeSlots,进行启发式整理。
  • 假如并未遇到共同的key,则判别一下是否有过期数据而且slotToExpunge == staleSlot,跟上述相同假如条件成立则阐明前后遍历时未找到过期数据,所以直接把slotToExpunge设置到当时方位即可(从当时方位进行勘探式整理)。
  • 跳出遍历后,则阐明没有找到共同的key进行掩盖,此时只能把staleSlot处过期的数据置空并把要存储的新数据存储进staleSlot处的Entry中(这便是上面set()办法说的本质上的替换操作)。
  • 最终便是为准备了好久的slotToExpunge作为参数发动勘探式整理,并在勘探式整理完后再发动启发式整理

总结:replaceStaleEntry(key, value, staleSlot)办法便是怎样样都会把需求存储的keyvalue存入当时staleSlot下标的Entry中,而且 staleSlot方位的过期数据总会被整理掉,该办法会调用勘探式和启发式整理


getEntry(ThreadLocal<?> key) 源码剖析

// getEntry()办法
private Entry getEntry(ThreadLocal<?> key) {
    // 核算Entry寄存的数组下标
    int i = key.threadLocalHashCode & (table.length - 1);
    // 取得Entry
    Entry e = table[i];
    // 假如Entry不为空且不是过期数据
    if (e != null && e.get() == key)
        // 直接回来
        return e;
    else
        // 不然调用getEntryAfterMiss()敞开寻址的办法向后查找
        return getEntryAfterMiss(key, i, e);
}
// getEntryAfterMiss()办法
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    // 直到Entry为空则退出循环
    while (e != null) {
        // 获取key
        ThreadLocal<?> k = e.get();
        // k == key 表明找到了回来
        if (k == key)
            return e;
        // 遇到过期数据启用勘探式整理办法整理
        if (k == null)
            expungeStaleEntry(i);
        else
            // 更新下标
            i = nextIndex(i, len);
        e = tab[i];
    }
    // 找不到回来null
    return null;
}

getEntry()流程概述

  • 经过参数key(方针key)核算出数组的下标方位,取得数组的下标方位的Entry。
  • 状况一:假如Entry不为空而且Entry的key与方针key共同时,直接回来Entry。
  • 状况二:假如Entry的key与方针key不共同时,调用getEntryAfterMiss()以敞开寻址的办法查找后续Entry(经过以当时下标方位开端向后遍历Entry数组)。
    • 假如后续的Entry的key为null,则阐明该Entry是过期数据,经过调用expungeStaleEntry(i)发动勘探式整理
    • 假如后续的Entry的key与方针key共同,则回来该Entry。
  • 假如上面两种状况都无法找到Entry,则回来null。

remove(ThreadLocal<?> key) 源码剖析

// remove()办法
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    // 获取key的数组下标
    int i = key.threadLocalHashCode & (len-1);
    // 从下标方位向后遍历Entry数组,直到Entry为空
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        // 当key与方针key相一起
        if (e.get() == key) {
            // 将key置空
            e.clear();
            // 发动勘探式整理铲除
            expungeStaleEntry(i);
            return;
        }
    }
}

6. rehash() & resize() 扩容机制

rehash() 源码剖析

经过前面的set()办法最终部分

if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();

启发式整理后假如Entry的数量size大于等于扩容阈值(数组的2/3)时,触发rehash()办法。

rehash()源码

private void rehash() {
    // 经过从头循环的办法判别是否有过期数据,有则履行勘探式整理
    expungeStaleEntries();
    // 假如整理后的Entry数量size大于阈值的3/4时则履行扩容操作
    if (size >= threshold - threshold / 4)
        resize();
}

expungeStaleEntries()源码

private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    // 从头遍历,遇到过期数据,则从当时方位进行勘探式整理
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        if (e != null && e.get() == null)
            expungeStaleEntry(j);
    }
}

rehash() & resize() 的触发条件

经过上面的源码可知

  • rehash() 触发条件:set()履行最终当启发式整理结束后 Entry的数量size >= 扩容阈值threshold(Entry数组的长度的2/3) 时触发
  • resize() 触发条件:rehash()履行而且expungeStaleEntries()底层调用的勘探式整理结束后 Entry的数量size >= 扩容阈值的3/4 时触发

resize() 源码剖析

private void resize() {
    // 获取旧的Entry数组
    Entry[] oldTab = table;
    // 旧数组的长度
    int oldLen = oldTab.length;
    // 新数组的长度
    int newLen = oldLen * 2;
    // 扩容新数组为旧数组两倍
    Entry[] newTab = new Entry[newLen];
    // 记载新数组下的Entry数量
    int count = 0;
    // 遍历旧数组
    for (Entry e : oldTab) {
        // 判别Entry是否存在
        if (e != null) {
            // 获取Entry的key
            ThreadLocal<?> k = e.get();
            // Entry是过期数据时,将值设置为null
            if (k == 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;
                // 更新新数组下的Entry数量
                count++;
            }
        }
    }
    // 设置新阈值
    setThreshold(newLen);
    // 设置新数量
    size = count;
    // 设置新数组
    table = newTab;
}

resize()流程概述

  • 将数组扩容为原先旧数组长度的两倍
  • 遍历旧的数组,关于Entry不为空且为过期数据的,直接将value设置为null,后续GC收回。
  • 关于Entry不为空且不为过期数据的,按新数组的长度从头核算下标,假如呈现哈希抵触,则敞开寻址,参加最近的Entry为空的方位。
  • 最终更新新的扩容阈值以及Entry数量以及Entry数组

7. 勘探式&启发式整理流程

过期Key整理办法一:勘探式整理expungeStaleEntry()

expungeStaleEntry() 源码

// 勘探式整理 staleSlot开端整理下标
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    // 1. 整理当时方位的Entry一起更新size
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;
    Entry e;
    int i;
    // 2. 从开端方位向后遍历Entry,直到遇到Entry为null时停止
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        // 获取当时的key
        ThreadLocal<?> k = e.get();
        // 判别是否为过期数据
        if (k == null) {
            // 是过期数据则直接整理当时Entry,一起更新size
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // 不是过期数据则再次获取Entry关于的下标
            int h = k.threadLocalHashCode & (len - 1);
            // 判别取得的下标是否是当时方位相同,不相同则从头定位
            if (h != i) {
                // 先置空当时方位
                tab[i] = null;
                // 从获取的下标处开端向后敞开寻址找到能寄存的下标
                while (tab[h] != null)
                    h = nextIndex(h, len);
                // 最终存储进空的Entry中
                tab[h] = e;
            }
        }
    }
    return i;
}

勘探式整理概述

  • 首要将传入参数 staleSlot(开端下标) 所在Entry整理(将Entry的value置null,而且将Entry置null)。
  • 从开端下标方位向后遍历Entry数组,取得关于下标对的key,判别key是否为null(判别当时方位的Entry是否过期):
    • 假如当时方位的Entry过期,则将当时方位的Entry整理(将Entry的value置null,而且将Entry置null)。
    • 假如当时方位的Entry未过期,则再次取得该Entry的下标,判别下标和当时方位索引是否相同,不相一起,则从获取的下标开端从头遍历Entry,直到遇到空的Entry时,放入当时空的Entry中。

所以勘探式整理的效果是从传入参数开端向后遍历Entry数组,遇到过期数据则整理,遇到未过期数据则从头选择寄存的数组方位,意图是让未过期数据离正确的下标更近一点(能够起到紧缩效果)

图示勘探式整理进程

深入理解ThreadLocal


过期Key整理办法二:启发式整理cleanSomeSlots()

cleanSomeSlots() 源码

// 启发式整理 (i表明开端下标,n表明数组长度)
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    // 从开端下标方位向后循环遍历,只会循环 数组长度的对数(比方长度为16则循环4次)
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        // 判别当Entry不为空且Entry又为过期数据时
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            // 从当时下标发动勘探式整理
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0); // 每次将n >>> 1 (即 n / 2)
    return removed;
}

启发式整理概述

  • 从传入的i开端下标方位向后查看,查看次数(方位)取决于数组长度的对数,比方数组长度为16,则查看log16 = 4次(即只会从开端下标向后查看4位)。
  • 遇到当时方位的Entry为过期数据时,会发动勘探式整理,把当时方位的下标传给expungeStaleEntry() 办法。

发动式整理只会查看一些单元格来整理过期数据。试探的扫描一些单元格,寻觅过期元素,也便是被废物收回的元素。当增加新元素set()或删除另一个过时元素时,将调用此函数。它履行对数扫描次数作为 不扫描(保存过期数据)与元素数量成份额的扫描次数 之间的平衡,使其能够铲除过期数据。