在Web项目中我们会使用线程本地变量来存储一些客户信息,以便于在业务上重复使用,那么就先看看ThreadLocal是什么。

What is ThreadLocal

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
For example, the class below generates unique identifiers local to each thread. A thread's id is assigned the first time it invokes ThreadId.get() and remains unchanged on subsequent calls.
import java.util.concurrent.atomic.AtomicInteger;


import java.util.concurrent.atomic.AtomicInteger;

public class ThreadId {
    // Atomic integer containing the next thread ID to be assigned
    private static final AtomicInteger nextId = new AtomicInteger(0);

    // Thread local variable containing each thread's ID
    private static final ThreadLocal<Integer> threadId =
        new ThreadLocal<Integer>() {
            @Override protected Integer initialValue() {
                return nextId.getAndIncrement();
        }
    };

    // Returns the current thread's unique ID, assigning it if necessary
    public static int get() {
        return threadId.get();
    }
}

Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).

以上是摘取自ThreadLocal源码的javadoc,大意是这个类提供了线程本地变量,每个线程可以通过get,set方法操作这个变量,这个变量是每个线程私有的。

实现流程

Thread Local内部具体的实现是这样的, 首先在Thread内部封装了一个map用于保存一些值, 然后ThreadLocal在get/set的时候, 首先拿到线程自身的那个map, 然后将自己作为key, 所要保存的值作为value, put进去, 这样就将具体的值保存在了每个线程自身上面(而不是ThreadLocal里面), 所以每个线程之间都会有独立的一份, 而不会相互影响。 至于为什么要用map而不是Object. 也很容易理解, 因为这样每个线程都可以保存不止一个ThreadLocal类型的属性。

我们可以看看他的源码,发现这个类其实并不复杂。

属性

    /**
     * 初始hashcode的值
     */
     private final int threadLocalHashCode = nextHashCode();

    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

这里我们只看threadLocalHashCode ,其他字段都是为它服务的。threadLocalHashCode 是用于标识每一个 ThreadLocal 的唯一性,它仅仅会在ThreadLocal的内部类ThreadLocalMap中使用,threadLocalHashCode 是专门为 ThreadLocalMap 优化过的,可以减少哈希碰撞,但是 threadLocalHashCode 用于普通 hash map 效果并不好。

threadLocalHashCode 初始值为 0,之后每次增加 0x61c88647,至于为什么是这个神奇的数字,后面解析 ThreadLocalMap 时再再详细解释。

初始化

第一种是构造函数 + set 方法:

ThreadLocal<String> x = new ThreadLocal<>();
x.set("Hello");

第二种是匿名内部类:

ThreadLocal<String> x = new ThreadLocal<String>() {
    @Override
    protected String initialValue() {
        return "hello";
    }
};

第三种是Java 8引入的 withInitial 静态方法:

ThreadLocal<String> x = ThreadLocal.withInitial(() -> "Hello");

现在更建议使用 withInitial 方法来代替匿名内部类初始化。

方法

对外的public方法有4个,其中一个是初始化方法,其余均是操作方法,ThreadLocal有一个ThreadLocalMap静态内部类,每个线程本地变量都是存储在这个Map中,这三个操作方法均是为了操作这个Map。

withInitial

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

get

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;
        }
    }
    return setInitialValue();
}

首先在当前线程的 ThreadLocalMap 中查询,若查询成功,则返回结果;若当前线程的 threadLocalsnull 或查询结果为 null,则调用 setInitialValue,并将其结果作为返回值,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);
        return value;
    }

首先通过 initialValue 获取本地变量的初始值:

protected T initialValue() {
  return null;
}

调用get方法如果此Map不存在首先初始化,创建此map,将线程为key,初始化的vlaue存入其中,注意此处的initialValue,我们可以覆盖此方法,在首次调用时初始化一个适当的值。

这里我们简单说下如何被java的垃圾收集机制收集,当我们不在使用时调用set(null),此时不在将引用指向该map,而线程退出时会执行资源回收操作,将申请的资源进行回收,其实就是将属性的引用设置为null。这时已经不在有任何引用指向该map,故而会被垃圾收集。

注意:如果ThreadLocal.set()进去的东西本来就是多个线程共享的同一个对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。

set

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

set方法也很容易理解:首先获取当前线程的引用,然后通过 getMap 获取当前线程的 ThreadLocalMap

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

其中 threadLocalsThread 类的一个字段,且默认值为 null

ThreadLocal.ThreadLocalMap threadLocals = null;

线程之间无法访问彼此的 threadLocals 变量,因此可以保证每个 ThreadLocal 实例只在 threadLocals 中出现一次,从而保证线程私有。

若当前线程的 ThreadLocalMap 不为空,则以当前 ThreadLocal 实例为 key,以参数值 value 为 value,将该 k-v 存入线程的 ThreadLocalMap 对象中:若当前线程的 ThreadLocalMap 不为空,则以当前 ThreadLocal 实例为 key,以参数值 value 为 value,将该 k-v 存入线程的 ThreadLocalMap 对象中:

map.set(this, value);

若当前线程的 ThreadLocalMap 为空,则为当前线程创建一个新的 ThreadLocalMap 值:

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

remove

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

removeThreadLocal 实例从 threadLocals 中删除,若删除后再 get 将导致本地变量 重新初始化,从而丢失之前的状态。

ThreadLocalMap

ThreadLocalMap 一个专门为 ThreadLocal key 定制实现的 hash map,它的所有方法只能在 ThreadLocal 类内部使用,其访问级别为 package,因此同一个包中的 Thread 可以声明一个 ThreadLocalMap 字段(t.threadLocals)。

ThreadLocalMap 是专用于维护线程本地变量的 hash map,这个类定义成了包私有的,这样是为了在 Thread 类中声明,从而在线程中维护一个 ThreadLocalMap 的引用。

这里注意到十分重要的一点:ThreadLocalMap$Entry 是 WeakReference(弱引用),并且键值 Key 为 ThreadLocal<?> 实例本身,这里使用了无限定的泛型通配符。

看一下 ThreadLocalMap 的构造函数,是惰性加载的,即只有当有元素要存放的时候才会构建。

ThreadLocalMap(java.lang.ThreadLocal<?> firstKey, Object firstValue) {
    // 初始化table数组
    table = new Entry[INITIAL_CAPACITY];
    // 用firstKey的threadLocalHashCode与初始大小16取模得到哈希值
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // 初始化该节点
    table[i] = new Entry(firstKey, firstValue);
    // 设置节点表大小为1
    size = 1;
    // 设定扩容阈值
    setThreshold(INITIAL_CAPACITY);
}

巧妙的取模操作

上面代码有一行是 ThreadLocalMap 的哈希算法,哈希算法就是根据 key 得到对应的 hash 值:

int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

这里的位运算的实质是去一个取模(求余)运算,决定一个 key 应该放在数组的哪个 index 上。

当取模运算中,除数是 2 的 N 次方时,既这个数用二进制表示的时候一定只有一个 1,比如 16,在 Java 的 Integer 中的二进制形式实质就是:

000000000000000000000000000010000

减 1 就是:

000000000000000000000000000001111

与被除数做与运算,被除数刚好高位就被消除,只剩下低位。即比除数大,但没有超过一倍的部分被保留。这刚好是取模(求余)运算。

之所以这么做,是因为位运算的效率要远高于普通的取模运算。

为什么要用 0x61c88647

// 每一个 ThreadLocal 实例对应的哈希值是不可变的
private final int threadLocalHashCode = nextHashCode();

private static AtomicInteger nextHashCode = new AtomicInteger();

// hash 函数递增的魔法值
private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

看完了取模操作,再看一下 firstKey.threadLocalHashCode 的具体实现:

// 每一个 ThreadLocal 实例对应的哈希值是不可变的
private final int threadLocalHashCode = nextHashCode();

private static AtomicInteger nextHashCode = new AtomicInteger();

// hash 函数递增的魔法值
private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

首先要注意到一点,threadLocalHashCode 是一个 final 的属性,而原子计数器变量 nextHashCode 和生成下一个哈希魔数的方法 nextHashCode() 是静态变量和静态方法,静态变量只会初始化一次。换而言之,每新建一个 ThreadLocal 实例,它内部的 threadLocalHashCode 就会增加 0x61c88647。举个例子:

//t1中的threadLocalHashCode变量为0x61c88647
ThreadLocal t1 = new ThreadLocal();
//t2中的threadLocalHashCode变量为0x61c88647 + 0x61c88647
ThreadLocal t2 = new ThreadLocal();
//t3中的threadLocalHashCode变量为0x61c88647 + 0x61c88647 + 0x61c88647
ThreadLocal t3 = new ThreadLocal();

threadLocalHashCode 是 ThreadLocalMap 结构中使用的哈希算法的核心变量,对于每个 ThreadLocal 实例,它的 threadLocalHashCode 是唯一的。

这里有一个 hash 递增的魔法值 0x61c88647,为什么选这样一个值呢?想要弄明白这一点首先复习一下斐波那契数列:

斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …
通项公式:F(n)=F(n-1)+F(n-2)

有趣的一点是,当 n 趋向于无穷大时,前一项与后一项的比值越来越逼近 0.618(或者说后一项与前一项的比值小数部分越来越逼近 0.618),而这个值 0.618 就被称为黄金分割数。

public static void main(String[] args) {
    // 黄金分割数 * 2的32次方 = 2654435769 - 这个是无符号32位整数的黄金分割数对应的那个值
    long c = (long) ((1L << 32) * (Math.sqrt(5) - 1) / 2);
    System.out.println("32 位无符号整型的黄金分割数:" + c);
    // 强制转换为带符号为的32位整型,值为-1640531527
    int i = (int) c;
    System.out.println("32 位有符号整型的黄金分割数:" + i);
}

结果如下:

32 位无符号整型的黄金分割数:2654435769
32 位有符号整型的黄金分割数:-1640531527

上面有一个 long 类型强转 int 类型的操作,最后得到的是一个负数。在 Java 中对 int 的越界处理是这样的:当一个数超过了 Integer.MAX_VALUE 后,Java 就会从 Integer 的另一头重新开始,也就是从 Integer.MIN_VALUE 往回倒推,所以最终越界数显示的结果就是 Integer.MIN_VALUE + (越界数 - Integer.MAX_VALUE) - 1

而 ThreadLocal 的哈希魔数正是 32 位有符号整型黄金分割数 1640531527 的十六进制 0x61c88647,通过相关理论研究和实践证明发现,使用这个魔数可以使对应的 key 经过 hash 算法后均匀分布到整个容器,可以实现了完美散列。

另外 hashCode & (size - 1) 功能与 hashCode % size 相同,相当于取余,但更加高效,HashMap 也使用了该技巧。

存储结构

ThreadLocalMap 的内部又是使用Entry作为存储结构。Entry 继承 WeakReference,以 ThreadLocal 实例为 key,以 Object 为 value。当 value 为 null 时,表示对应的 key 不再使用,因此可以从 table 中删除,该类 entry 被称为 “stale entry”。

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

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

为什么使用 WeakReference 作为其 key 的类型?

源码中是这样解释的:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了应对大量并且长期地使用 ThreadLocal,哈希表使用了弱应用作为其 key 的类型。

大量使用意味着对应的 key 的数目会很多,而长期使用则是由于 ThreadLocal 的生命周期和线程的生命周期一样长。

如果这里使用普通的 key-value 形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在 GC 分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。弱引用是 Java 中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次 GC。当某个 ThreadLocal 已经没有强引用可达,则随着它被垃圾回收,在 ThreadLocalMap 里对应的 Entry 的键值会失效,这为 ThreadLocalMap 本身的垃圾清理提供了便利。

看起来结局皆大欢喜,引用 ThreadLocal 的对象被回收 -> ThreadLocalMap 弱引用 key 被回收,真的没问题了吗?既然是一个 map,那就说明数据结构是 key-value 对,现在仅仅 K 使用了弱引用然后被回收了,那么 value 呢?value 为什么不使用弱引用类型?过期的 value 会被回收吗?

事实上当 ThrealLocalMap 的 key 被回收之后,对应的 value 会在下一次调用 setgetremove 的时候被清除掉。所以实际上从ThreadLocal设计角度来说是不会导致内存泄露的。

构造

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    //1.创建和初始化table容量 初始化容量为16 
    table = new Entry[INITIAL_CAPACITY];
    //2.快速hash获取下标地址
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    //3.创建Entry,存放第一个数据
    table[i] = new Entry(firstKey, firstValue);
    //4.设置存储个数
    size = 1;
    //5.设置扩容阀值
    setThreshold(INITIAL_CAPACITY);
}

getEntry

这个方法会被ThreadLocal的get方法直接调用,用于获取map中某个ThreadLocal存放的值。如果你有稍微关注nextIndex(int i, int len),prevIndex(int i, int len)方法,你会发现这个获取下一个/上一个索引的方法是一个环形的!看一下 ThreadLocalMap 的 getEntry 方法的源码:

private Entry getEntry(ThreadLocal<?> key) {
    //通过散列函数计算数组下标
    int i = key.threadLocalHashCode & (table.length - 1);   
    Entry e = table[i];
    //对应的entry存在且未失效且弱引用指向的ThreadLocal就是key,则命中返回
    if (e != null && e.get() == key)                        
        return e;
    else
        return getEntryAfterMiss(key, i, e);                
}

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

    //利用线性探测法来寻找key所在的位置  
    while (e != null) {                
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        //如果当前遍历到的key已经被回收了,那么进行清理
        if (k == null)                 
            expungeStaleEntry(i);
        else
            //利用环形数组的原理来变化i值
            i = nextIndex(i, len);     
        e = tab[i];
    }
    return null;
}
/**
 * 这个函数是ThreadLocal中核心清理函数,它做的事情很简单:
 * 就是从staleSlot开始遍历,将无效(弱引用指向对象被回收)清理,即对应entry中的value置为null,将指向这个entry的table[i]置为null,直到扫到空entry。
 * 另外,在过程中还会对非空的entry作rehash。
 * 可以说这个函数的作用就是从staleSlot开始清理连续段中的slot(断开强引用,rehash slot等)
 */
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 因为entry对应的ThreadLocal已经被回收,value设为null,显式断开强引用
    tab[staleSlot].value = null;
    // 显式设置该entry为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();
        // 清理对应ThreadLocal已经被回收的entry
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            /*
             * 对于还没有被回收的情况,需要做一次rehash。
             * 
             * 如果对应的ThreadLocal的ID对len取模出来的索引h不为当前位置i,
             * 则从h向后线性探测到第一个空的slot,把当前的entry给挪过去。
             */
            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.
                 *
                 * 这段话提及了Knuth高德纳的著作TAOCP(《计算机程序设计艺术》)的6.4章节(散列)
                 * 中的R算法。R算法描述了如何从使用线性探测的散列表中删除一个元素。
                 * R算法维护了一个上次删除元素的index,当在非空连续段中扫到某个entry的哈希值取模后的索引
                 * 还没有遍历到时,会将该entry挪到index那个位置,并更新当前位置为新的index,
                 * 继续向后扫描直到遇到空的entry。
                 *
                 * ThreadLocalMap因为使用了弱引用,所以其实每个slot的状态有三种也即
                 * 有效(value未回收),无效(value已回收),空(entry==null)。
                 * 正是因为ThreadLocalMap的entry有三种状态,所以不能完全套高德纳原书的R算法。
                 *
                 * 因为expungeStaleEntry函数在扫描过程中还会对无效slot清理将之转为空slot,
                 * 如果直接套用R算法,可能会出现具有相同哈希值的entry之间断开(中间有空entry)。
                 */
                while (tab[h] != null) {
                    h = nextIndex(h, len);
                }
                tab[h] = e;
            }
        }
    }
    // 返回staleSlot之后第一个空的slot索引
    return i;
}

set

ThreadLocalMap 使用哈希表存储 key,哈希表的优点是查找和插入速度比较快,但是缺点是不同的 key 经过哈希函数计算以后得到的数组下标可能存在冲突。那么如何解决冲突呢,ThreadLocalMap 使用的方法是线性探测法,即当 key 经过哈希函数计算得到一个下标,但是却发现该下标的位置已经有元素了,那么就继续找下一个位置,直到找到一个符合条件的位置。

private void set(ThreadLocal<?> key, Object value) {
//调用set之前已经做过判断,所以table已经初始化了
    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)]) {
        ThreadLocal<?> k = e.get();
        // 找到对应的entry
        if (k == key) {
            e.value = value;
            return;
        }
        // 替换失效的entry
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    //清理key已经过期清理的slot,如果存储个数已经大于扩容阀值,则扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold) {
        rehash();
    }
}

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // 向前扫描,查找最前的一个无效slot
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len)) {
        if (e.get() == null) {
            slotToExpunge = i;
        }
    }

    // 向后遍历table
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // 找到了key,将其与无效的slot交换
        if (k == key) {
            // 更新对应slot的value值
            e.value = value;

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

            /*
             * 如果在整个扫描过程中(包括函数一开始的向前扫描与i之前的向后扫描)
             * 找到了之前的无效slot则以那个位置作为清理的起点,
             * 否则则以当前的i作为清理起点
             */
            if (slotToExpunge == staleSlot) {
                slotToExpunge = i;
            }
            // 从slotToExpunge开始做一次连续段的清理,再做一次启发式清理
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // 如果当前的slot已经无效,并且向前扫描过程中没有无效slot,则更新slotToExpunge为当前位置
        if (k == null && slotToExpunge == staleSlot) {
            slotToExpunge = i;
        }
    }

    // 如果key在table中不存在,则在原地放一个即可
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // 在探测过程中如果发现任何无效slot,则做一次清理(连续段清理+启发式清理)
    if (slotToExpunge != staleSlot) {
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    }
}

/**
 * 启发式地清理slot,
 * 正常情况下如果log n次扫描没有发现无效slot,函数就结束了
 * 但是如果发现了无效的slot,将n置为table的长度len,做一次连续段的清理
 * 再从下一个空的slot开始继续扫描
 * 
 * 这个函数有两处地方会被调用,一处是插入的时候可能会被调用,另外个是在替换无效slot的时候可能会被调用,
 * 区别是前者传入的n为元素个数,后者为table的容量
 * @param i 对应entry是非无效(指向的ThreadLocal没被回收,或者entry本身为空)
 * @param n 用于控制控制扫描次数的
 */
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        // i在任何情况下自己都不会是一个无效slot,向前环形扫描
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            // 扩大扫描控制因子
            n = len;
            removed = true;
            //调用清理函数,i就是下一次向前探测的初始位置,
            //因为在[旧i,新i]之间的无效slot都被清理了
            i = expungeStaleEntry(i);
        }
        //n >>>= 1 表示 n = n >>> 1,>>>表示无符号右移
    } while ((n >>>= 1) != 0);
    return removed;
}


private void rehash() {
    // 做一次全量清理
    expungeStaleEntries();

    /*
     * 因为做了一次清理,所以size很可能会变小。
     * ThreadLocalMap这里的实现是调低阈值来判断是否需要扩容,
     * 如 threshold默认为len*2/3,所以这里的threshold - threshold / 4相当于len/2
     */
    if (size >= threshold - threshold / 4) {
        resize();
    }
}

/*
 * 做一次全量清理
 */
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) {
            /*
             * 个人觉得这里可以取返回值,如果大于j的话取了用,这样也是可行的。
             * 因为expungeStaleEntry执行过程中是把连续段内所有无效slot都清理了一遍了。
             */
            expungeStaleEntry(j);
        }
    }
}

/**
 * 扩容,因为需要保证table的容量len为2的幂,所以扩容即扩大2倍
 */
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null; 
            } else {
                // 线性探测来存放Entry
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null) {
                    h = nextIndex(h, newLen);
                }
                newTab[h] = e;
                count++;
            }
        }
    }

    setThreshold(newLen);
    size = count;
    table = newTab;
}

什么时候无用的 Entry 会被清理:

  • Thread 结束的时候
  • 插入元素时,发现 staled entry,则会进行替换并清理
  • 插入元素时,ThreadLocalMapsize 达到 threshold,并且没有任何 staled entries 的时候,会调用 rehash 方法清理并扩容
  • 调用 ThreadLocalMapremove 方法或set(null)

尽管不会造成内存泄露,但是可以看到无用的 Entry 只会在以上四种情况下才会被清理,这就可能导致一些 Entry 虽然无用但还占内存的情况。因此,我们在使用完 ThreadLocal 后一定要remove一下,保证及时回收掉无用的 Entry。

特别地,当应用线程池的时候,由于线程池的线程一般会复用,Thread 不结束,这时候用完更需要 remove 了。

ThreadLocal与内存泄漏

认为ThreadLocal会引起内存泄漏的说法是因为如果一个ThreadLocal对象被回收了,我们往里面放的value对于【当前线程->当前线程的threadLocals(ThreadLocal.ThreadLocalMap对象)->Entry数组->某个entry.value】这样一条强引用链是可达的,因此value不会被回收。
认为ThreadLocal不会引起内存泄漏的说法是因为ThreadLocal.ThreadLocalMap源码实现中自带一套自我清理的机制。

之所以有关于内存泄露的讨论是因为在有线程复用如线程池的场景中,一个线程的寿命很长,大对象长期不被回收影响系统运行效率与安全。如果线程不会复用,用完即销毁了也不会有ThreadLocal引发内存泄露的问题。《Effective Java》一书中的第6条对这种内存泄露称为unintentional object retention(无意识的对象保留)。

当我们仔细读过ThreadLocalMap的源码,我们可以推断,如果在使用的ThreadLocal的过程中,显式地进行remove是个很好的编码习惯,这样是不会引起内存泄漏。
那么如果没有显式地进行remove呢?只能说如果对应线程之后调用ThreadLocal的get和set方法都有很高的概率会顺便清理掉无效对象,断开value强引用,从而大对象被收集器回收。

ThreadLocal建议

  1. ThreadLocal应定义为静态成员变量。
  2. 能通过传值传递的参数,不要通过ThreadLocal存储,以免造成ThreadLocal的滥用。
  3. 在线程池的情况下,在ThreadLocal业务周期处理完成时,最好显式的调用remove()方法,清空”线程局部变量”中的值。
  4. 正常情况下使用ThreadLocal不会造成内存溢出,弱引用的只是threadLocal,保存的值依然是强引用的,如果threadLocal依然被其他对象强引用,”线程局部变量”是无法回收的。

应用

Spring项目中我们可能会需要多次使用用户信息,时区,以及某些标识,那么这时候就需要一个线程本地变量存储这些数据,然后当一个请求进入后在不同的业务处理中进行处理能够方便地拿到这些信息。

注:Tomcat维护了一个线程池,每一个HTTP请求都会从线程池中取一个空闲线程,但是切记在一个线程中再开线程或者使用Java8的并行流都会导致无法取到ThreadLocal!

参考:

https://baixin.ink/2019/09/24/threadlocal/

http://songkun.me/2018/09/09/2018-09-09-source-code-reading-threadlocal/

http://vence.github.io/2016/05/28/threadlocal-info/

http://666-666.club/?p=669

https://juejin.im/post/5e0cbbe1e51d45414b74dc7d#heading-10

https://zhuanlan.zhihu.com/p/37343288

https://blog.csdn.net/u010887744/article/details/54730556

https://www.cnblogs.com/micrari/p/6790229.html