如何有效解决ThreadLocal在多线程环境下引发的数据隔离和线程安全问题?

2026-04-19 12:301阅读0评论SEO问题
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计3557个文字,预计阅读时间需要15分钟。

如何有效解决ThreadLocal在多线程环境下引发的数据隔离和线程安全问题?

背景:项目主体架构Spring Cloud。使用ThreadLocal保存线程数据,但发现保存的数据不会随着线程的结束而消失。

解决:线程结束后,ThreadLocal保存的数据要调用remove()方法清除数据。

原理:因为每个ThreadLocal变量在ThreadLocalMap中对应一个ThreadLocal对象,ThreadLocalMap使用ThreadLocal作为key,value是保存的数据。当线程结束时,ThreadLocalMap中的ThreadLocal对象不会被回收,导致数据无法随线程结束而消失。调用remove()方法可以清除ThreadLocalMap中对应的ThreadLocal对象,从而释放数据。

背景

项目主体架构Spring Cloud。使用ThreadLocal保存线程数据,但是发现保存的数据不会随着线程的结束而消失。

解决

线程结束后ThreadLocal保存的数据要调用remove()清除数据。

原理

因为每个线程是从SpringBoot线程池里拿的,如果拿到同一个线程并且该线程的ThreadLocal没有remove数据,那么就会存在上述背景问题。以下是测试代码。

private static ThreadLocal<Long> threadLocal = new ThreadLocal<>(); /** * ThreadLocal 在保存值后,如果不remove,保存的值是不会清除的 * 输出: * 第一次请求 * threadId: 51 * Value is null, and set value * 第二次请求 * threadId: 51 * value: 51 */ @GetMapping("testThreadLocalNoRm") public String testThreadLocalNoRm() { Thread t = Thread.currentThread(); long tId = t.getId(); System.out.println("threadId: " + tId); Long value = threadLocal.get(); if (value != null) { // 第二次请求,如果没有remove值tId,则打印值 System.out.println("value: " + value); } else { // 第一次请求 ThreadLocal没值,会先保存值tId System.out.println("Value is null, and set value"); threadLocal.set(tId); } // 请求完成remove清除值 // threadLocal.remove(); return "success"; }

第一次请求testThreadLocalNoRm,如线程id: 51,value为空,把线程id存入ThreadLocal。

第二次请求testThreadLocalNoRm,同一个线程51,value不为空,取到第一次请求存入的值51。

进一步研究

怀着对ThreadLocal的好奇,进一步研究ThreadLocal,发现它也是一种抽象数据类型 (abstract data type,ADT,带有一组操作的一些对象的集合)

ThreadLocal里有个静态内部类ThreadLocalMap,是一个自定义的Hash Map,是一个散列表,ThreadLocal存储值是以键值对保存在ThreadLocalMap.Entry里,Entry继承了WeakReference,其中key是当前ThreadLocal实例。Key是弱引用。后面remove()函数就是利用这个弱引用机制。

static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); }

ThreadLocal.ThreadLocalMap是Thread的实例变量threadLocals,该实例变量是包访问权限,所以说ThreadLocal是线程的内部数据,或者说线程本地变量。

Thread.threadLocals

/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal.ThreadLocalMap.createMap

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


ThreadLocal如何保存数据

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

根据当前线程找到ThreadLocalMap,不为null以当前ThreadLocal实例为key保存value,否则创建ThreadLocalMap。

ThreadLocalMap.set()源码

private void set(ThreadLocal<?> key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. 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(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }

根据当前ThreadLocal实例的threadLocalHashCode,和(len - 1)与操作,计算Entry数组的下标。len是该数组长度。初始长度16,且必须是2的幂。

如果定位到下标的槽位不为空,即槽位存在Entry,key与槽位Entry的k一样,替换槽位Entry的value,如果槽位Entry的k是null值,替换旧Entry。

如果下标槽位空的,则在该槽位创建一个Entry(key, value);

如果有过时Entry清除掉,同时达到Entry数组的扩容阀值,该阀值是2/3的数组长度取整。默认是10,即16 * 2/3。

如果遇到槽位有值,且k != key,k不为空,这就产生冲突了,这里解决冲突用线性探测法,nextIndex(i, len),就是往下一个槽位探测,找到一个空的槽位插入,nextIndex函数是以环形的方式探测下一槽位的,探测到最后一个槽位后又从头开始探测。

private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }

rehash()如何扩容的

/** * Re-pack and/or re-size the table. First scan the entire * table removing stale entries. If this doesn't sufficiently * shrink the size of the table, double the table size. */ private void rehash() { expungeStaleEntries(); // Use lower threshold for doubling to avoid hysteresis if (size >= threshold - threshold / 4) resize(); }

删除过期Entry,删除过期Entry之后的size,就是还剩下多少个Entry,这个size >= 8则进行扩容。默认threshold是10。

threshold = len * 2/3,扩容阀值

threshold - threshold / 4 = threshold * (1 – 1/4) = threshold * 3/4 = len * 2/3 * 3/4 = len * 1/2; 实际扩容阀值

所以,大于阀值只是进入扩容准备阶段,准备阶段清除过期Entry后,还剩的Entry数量size,大于或者等于len * 1/2,才会进入实际扩容。

扩容会重新创建一个是原来两倍长度的Entry数组,再将原数组的Entry重新哈希定位下标存入新数组。过程中遇到某个Entry不为空的key == null的,把value也置null,方便GC回收。第一次扩容之后Entry数组长度32,阀值21,实际扩容阀值16。

resize()源码

/** * Double the capacity of the table. */ 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; // Help the GC } else { int h = k.threadLocalHashCode & (newLen - 1); while (newTab[h] != null) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } setThreshold(newLen); size = count; table = newTab; }

ThreadLocal如何查询数据

ThreadLocal的get()函数获取set(key, value)存入的value,key是当前ThreadLocal实例。

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,即Thread里的实例变量threadLocals。

ThreadLocalMap不为空,根据当前ThreadLocal实例获取Entry。

通过上面的介绍Entry是一个键值对,不为空返回里面的value。

如果ThreadLocalMap为空,为其设一个初始值,类似上面ThreadLocal.set(),只是value是null。

ThreadLocal如何删除数据

ThreadLocal.remove()源码

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

ThreadLocal通过remove()函数删除set入的数据。取得当前线程的ThreadLocalMap,删除当前ThreadLocal实例的Entry。先是把Entry的弱引用key置空,该引用的那块内存会被GC回收,以此上面那些key==null的Entry都是过期的,待GC回收的ThreadLocal实例。e.clear()就是将key引用置空。删除过期Entry则将value置空,和该Entry槽位置空。

关于4种引用,分别是,强引用、软引用、弱引用、虚引用。

  • 强引用对象不会被垃圾回收,甚至OOM也不会被回收。
  • 软引用SoftReference,堆内存紧张时 GC回收,否则不回收,所以软引用不会引起OOM。
  • 弱引用WeakReference,GC时,无论是否堆内存是否紧张都会回收。
  • 虚引用PhantomReference,用于跟踪对象回收过程,虚引用通过get()是拿不到引用对象的。

测试ThreadLocal的GC

public class ThreadLocalGC { public static class User { private String name; public User(String name) { this.name = name; } // 监控一下User垃圾回收 @Override protected void finalize() { System.out.println("User GC"); } @Override public String toString() { return "User{" + "name='" + name + '\'' + '}'; } } public static class UserRun implements Runnable { private long sleepTime; public UserRun(long sleepTime) { this.sleepTime = sleepTime; } private static ThreadLocal<User> TL = new ThreadLocal<User>() { // 监控ThreadLocal垃圾回收 @Override protected void finalize() throws Throwable { super.finalize(); System.out.println(this + ": TL GC"); } }; @Override public void run() { User user = new User("Joy Boy"); TL.set(user); // user置为null,释放User,System.gc()会回收,不置null,System.gc()不会回收 // 如果内存紧张不管是否释放都会回收User // user = null; System.out.println(Thread.currentThread().getId() + ": " + TL.get()); System.out.println("TL1: " + TL); TL.remove(); // -Xmx1m最大堆1m 申请一个大内存触发GC,制造内存紧张 byte[] b = new byte[1024 * 500]; // 手动GC,内存紧张的时候不用手动GC也会触发GC // System.gc(); // 这里重新new一个TL会释放上面的TL TL = new ThreadLocal<User>() { @Override protected void finalize() { System.out.println(this + ": TL GC"); } }; System.out.println(Thread.currentThread().getId() + ": " + TL.get()); System.out.println("TL2: " + TL); try { // 休眠一下可以看到GC,让垃圾回收线程执行完,以此监测GC Thread.sleep(sleepTime); } catch (InterruptedException e) { e.printStackTrace(); } } } /** * 设置JVM参数方便测试 * -Xmx1m 设置堆大小1m * -XX:+PrintGCDetails 打印GC详情 * * 打印信息 * [GC (Allocation Failure) 1099K->676K(1536K), 0.0007083 secs] * [Full GC (Ergonomics) 804K->304K(1536K), 0.0060029 secs] * 12: User{name='Joy Boy'} * TL1: com.zx.threads.thread.local.ThreadLocalGC$UserRun$1@21a66f33 * 12: null * TL2: com.zx.threads.thread.local.ThreadLocalGC$UserRun$2@2f618577 * com.zx.threads.thread.local.ThreadLocalGC$UserRun$1@21a66f33: TL GC * User GC */ public static void main(String[] args) throws InterruptedException { new Thread(new UserRun(1000)).start(); } }

为了方便测试运行的堆内存设置1M,Entry的value是User,key是TL。User和TL都重写了finalize(),用于跟踪GC。启个UserRun线程,测试垃圾回收过程。对重点代码作了解释。TL.remove()之后,在Entry释放了TL,释放User。TL重新new了,所以TL1对象被GC了,User也被GC了,如果TL还有引用是不会被GC的,重新new了,TL1没有被引用,才会被GC。一般生产环境中,不会重新new ThreadLocal(),所以就算Entry释放了TL,ThreadLocal也不会被GC。只有value被释放了之后会被GC。 最终Entry也会被GC。因此如果不remove()释放掉Entry,可能会导致OOM。

同一线程每new一个ThreadLocal,生成一个Entry,实现原理

通过阅读ThreadLocal源码,它维护着一个ThreadLocalMap,该Map维护着一个Entry数组。在同一线程中,每new一个ThreadLocal就是一个Entry,根据threadLocalHashCode和数组长度减一,与运算,即threadLocalHashCode & (len - 1)计算出数组下标,插入数组。如果同一线程中创建了多个ThreadLocal,那么生成的下标肯定是不一样的,而数组长度是一定的,默认长度是16,所以threadLocalHashCode肯定是会变化的,才会算出不一样下标。问题是threadLocalHashCode是如何变化的?

为搞清楚这个问题,先看看ThreadLocal的threadLocalHashCode相关源码。

private final int threadLocalHashCode = nextHashCode(); private static AtomicInteger nextHashCode = new AtomicInteger(); private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }

在ThreadLocal中threadLocalHashCode被声明为,私有的final整型基本数据类型,且调用静态方法nextHashCode()赋值。这个final关键字和调用nextHashCode()为其实现了同一线程中,每new一个ThreadLocal就会生成新threadLocalHashCode,final定义的字段(没有static),构造函数完成之前,不能初始化该字段,即构造函数完成之前,该字段对其他线程不可见。所以每次new ThreadLocal()都会初始化一个threadLocalHashCode,这个初始化就是调用nextHashCode();

通过相关源码的字节码指令也可以看出

/** * public <init>()V * L0 * LINENUMBER 5 L0 * ALOAD 0 * INVOKESPECIAL java/lang/Object.<init> ()V * L1 * LINENUMBER 14 L1 * ALOAD 0 * INVOKESTATIC com/zx/finals/FinalByMethod.nextHashCode ()I * PUTFIELD com/zx/finals/FinalByMethod.threadLocalHashCode : I * RETURN * L2 * LOCALVARIABLE this Lcom/zx/finals/FinalByMethod; L0 L2 0 * MAXSTACK = 2 * MAXLOCALS = 1 */

public <init>()V,就是new ThreadLocal()执行的,

INVOKESTATIC com/zx/finals/FinalByMethod.nextHashCode ()I,

就是执行静态方法nextHashCode ()。

PUTFIELD com/zx/finals/FinalByMethod.threadLocalHashCode : I,

赋值给threadLocalHashCode

0x61c88647猜想

ThreadLocal.ThreadLocalMap就是一个散列表,其散列函数通过ThreadLocal的散列码跟散列表长度取模实现,ThreadLocal的散列码是0x61c88647的叠加得出。

ThreadLocal散列叠加数0x61c88647,关于这个数的猜想

在说这个散列叠加数之前,先了解一下黄金分割

数学定义:把一条线段分割为两部分,使较大部分与全长的比值等于较小部分与较大的比值,则这个比值即为黄金分割。其比值是 2 / (√5 + 1) = (√5 - 1) / 2,近似值为0.618,通常用希腊字母 Ф [fi:] 表示这个值。有些文献用 φ (Ф的小写) 表示斐波那契黄金分割数 (√5 - 1) / 2 ≈ 0.618,用 Ф 表示黄金分区率的倒数 1 / φ,(√5 + 1) / 2 ≈ 1.618

A————————C——————B,设一条线段AB的长度为a,C点在靠近B点的黄金分割点上,且AC为b,则b与a的比叫作黄金比

0x61c88647的十进制表示1640531527,假设这个数是a的黄金分割点,也就是C点,以下是关于这个假设的推理:

1.当这个数是AC,求AB

2.当这个数是BC,求AB

如何有效解决ThreadLocal在多线程环境下引发的数据隔离和线程安全问题?


第一种假设,AB:2654435770,其二进制表示10011110001101110111100110111010

第二种假设,AB:4294967297,其二进制表示100000000000000000000000000000001,

在计算机里更倾向于第二种假设,因其Java二进制位移表示(1L << 32) + 1,方便计算加一可以忽略表示为

Integer.MAX_VALUE = 1 << 31 = 2³¹ = -2147483648,转成Long表示,1L << 31 = 2147483648

1L << 32 = 2147483648 * 2 = 4294967296。

逆向证明过程

AB * ((√5 - 1) / 2),其Java代码表示

0x61c88647 = (1L << 32) - 2654435769 = 1640531527;

2654435769其实就是较长的那段AC,1640531527就是较短那段BC。

ThreadLocal的threadLocalHashCode,这个线程本地变量散列码,是通过0x61c88647叠加来起到减少冲突的作用

ThreadLocal的nextHashCode() 就是叠加函数,准确说是扰乱函数,通过叠加0x61c88647来扰乱散列码,以此减少冲突。

ThreadLocalMap散列表通过 threadLocalHashCode & (len - 1) 散列函数寻址,len是散列表长度

threadLocalHashCode & (len - 1) 等同于threadLocalHashCode % len (threadLocalHashCode mod len)

线程本地变量散列码和长度减一与运算,求余数,

(len - 1),len是2的幂,默认是(2⁴ - 1),二进制表示1111,(32 - 4)位高位补0,所以与运用算只算最低4位,threadLocalHashCode最低4位就是余数

threadLocalHashCode的散列是运用乘法散列方案,设 k = 0x61c88647,则散列函数 h(K) = k, 2k, 3k...nk,

ThreadLocalMap是运用除法散列方案 h(K) = nk mod len


关于黄金分割使得散列均匀分布早有证明,这个证明中斐波那契数列起着重要作用,参考Donald E.Knuth The Art of Computer Programming 6.4

如果非要说0x61c88647跟黄金分割有关系,刚好是分割了Integer.MAX_VALUE的两倍的黄金分割点,

测试用AC段2654435769作为这个分割数也能均匀分布散列码。

最终猜测只要这个分割数足够大,那么每次叠加都是唯一散列码,用这个唯一码再去取模散列表长度时,出现冲突概率会非常小,加上散列表的扩容机制,这个冲突概率就更小了。

本文共计3557个文字,预计阅读时间需要15分钟。

如何有效解决ThreadLocal在多线程环境下引发的数据隔离和线程安全问题?

背景:项目主体架构Spring Cloud。使用ThreadLocal保存线程数据,但发现保存的数据不会随着线程的结束而消失。

解决:线程结束后,ThreadLocal保存的数据要调用remove()方法清除数据。

原理:因为每个ThreadLocal变量在ThreadLocalMap中对应一个ThreadLocal对象,ThreadLocalMap使用ThreadLocal作为key,value是保存的数据。当线程结束时,ThreadLocalMap中的ThreadLocal对象不会被回收,导致数据无法随线程结束而消失。调用remove()方法可以清除ThreadLocalMap中对应的ThreadLocal对象,从而释放数据。

背景

项目主体架构Spring Cloud。使用ThreadLocal保存线程数据,但是发现保存的数据不会随着线程的结束而消失。

解决

线程结束后ThreadLocal保存的数据要调用remove()清除数据。

原理

因为每个线程是从SpringBoot线程池里拿的,如果拿到同一个线程并且该线程的ThreadLocal没有remove数据,那么就会存在上述背景问题。以下是测试代码。

private static ThreadLocal<Long> threadLocal = new ThreadLocal<>(); /** * ThreadLocal 在保存值后,如果不remove,保存的值是不会清除的 * 输出: * 第一次请求 * threadId: 51 * Value is null, and set value * 第二次请求 * threadId: 51 * value: 51 */ @GetMapping("testThreadLocalNoRm") public String testThreadLocalNoRm() { Thread t = Thread.currentThread(); long tId = t.getId(); System.out.println("threadId: " + tId); Long value = threadLocal.get(); if (value != null) { // 第二次请求,如果没有remove值tId,则打印值 System.out.println("value: " + value); } else { // 第一次请求 ThreadLocal没值,会先保存值tId System.out.println("Value is null, and set value"); threadLocal.set(tId); } // 请求完成remove清除值 // threadLocal.remove(); return "success"; }

第一次请求testThreadLocalNoRm,如线程id: 51,value为空,把线程id存入ThreadLocal。

第二次请求testThreadLocalNoRm,同一个线程51,value不为空,取到第一次请求存入的值51。

进一步研究

怀着对ThreadLocal的好奇,进一步研究ThreadLocal,发现它也是一种抽象数据类型 (abstract data type,ADT,带有一组操作的一些对象的集合)

ThreadLocal里有个静态内部类ThreadLocalMap,是一个自定义的Hash Map,是一个散列表,ThreadLocal存储值是以键值对保存在ThreadLocalMap.Entry里,Entry继承了WeakReference,其中key是当前ThreadLocal实例。Key是弱引用。后面remove()函数就是利用这个弱引用机制。

static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); }

ThreadLocal.ThreadLocalMap是Thread的实例变量threadLocals,该实例变量是包访问权限,所以说ThreadLocal是线程的内部数据,或者说线程本地变量。

Thread.threadLocals

/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal.ThreadLocalMap.createMap

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


ThreadLocal如何保存数据

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

根据当前线程找到ThreadLocalMap,不为null以当前ThreadLocal实例为key保存value,否则创建ThreadLocalMap。

ThreadLocalMap.set()源码

private void set(ThreadLocal<?> key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. 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(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }

根据当前ThreadLocal实例的threadLocalHashCode,和(len - 1)与操作,计算Entry数组的下标。len是该数组长度。初始长度16,且必须是2的幂。

如果定位到下标的槽位不为空,即槽位存在Entry,key与槽位Entry的k一样,替换槽位Entry的value,如果槽位Entry的k是null值,替换旧Entry。

如果下标槽位空的,则在该槽位创建一个Entry(key, value);

如果有过时Entry清除掉,同时达到Entry数组的扩容阀值,该阀值是2/3的数组长度取整。默认是10,即16 * 2/3。

如果遇到槽位有值,且k != key,k不为空,这就产生冲突了,这里解决冲突用线性探测法,nextIndex(i, len),就是往下一个槽位探测,找到一个空的槽位插入,nextIndex函数是以环形的方式探测下一槽位的,探测到最后一个槽位后又从头开始探测。

private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }

rehash()如何扩容的

/** * Re-pack and/or re-size the table. First scan the entire * table removing stale entries. If this doesn't sufficiently * shrink the size of the table, double the table size. */ private void rehash() { expungeStaleEntries(); // Use lower threshold for doubling to avoid hysteresis if (size >= threshold - threshold / 4) resize(); }

删除过期Entry,删除过期Entry之后的size,就是还剩下多少个Entry,这个size >= 8则进行扩容。默认threshold是10。

threshold = len * 2/3,扩容阀值

threshold - threshold / 4 = threshold * (1 – 1/4) = threshold * 3/4 = len * 2/3 * 3/4 = len * 1/2; 实际扩容阀值

所以,大于阀值只是进入扩容准备阶段,准备阶段清除过期Entry后,还剩的Entry数量size,大于或者等于len * 1/2,才会进入实际扩容。

扩容会重新创建一个是原来两倍长度的Entry数组,再将原数组的Entry重新哈希定位下标存入新数组。过程中遇到某个Entry不为空的key == null的,把value也置null,方便GC回收。第一次扩容之后Entry数组长度32,阀值21,实际扩容阀值16。

resize()源码

/** * Double the capacity of the table. */ 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; // Help the GC } else { int h = k.threadLocalHashCode & (newLen - 1); while (newTab[h] != null) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } setThreshold(newLen); size = count; table = newTab; }

ThreadLocal如何查询数据

ThreadLocal的get()函数获取set(key, value)存入的value,key是当前ThreadLocal实例。

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,即Thread里的实例变量threadLocals。

ThreadLocalMap不为空,根据当前ThreadLocal实例获取Entry。

通过上面的介绍Entry是一个键值对,不为空返回里面的value。

如果ThreadLocalMap为空,为其设一个初始值,类似上面ThreadLocal.set(),只是value是null。

ThreadLocal如何删除数据

ThreadLocal.remove()源码

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

ThreadLocal通过remove()函数删除set入的数据。取得当前线程的ThreadLocalMap,删除当前ThreadLocal实例的Entry。先是把Entry的弱引用key置空,该引用的那块内存会被GC回收,以此上面那些key==null的Entry都是过期的,待GC回收的ThreadLocal实例。e.clear()就是将key引用置空。删除过期Entry则将value置空,和该Entry槽位置空。

关于4种引用,分别是,强引用、软引用、弱引用、虚引用。

  • 强引用对象不会被垃圾回收,甚至OOM也不会被回收。
  • 软引用SoftReference,堆内存紧张时 GC回收,否则不回收,所以软引用不会引起OOM。
  • 弱引用WeakReference,GC时,无论是否堆内存是否紧张都会回收。
  • 虚引用PhantomReference,用于跟踪对象回收过程,虚引用通过get()是拿不到引用对象的。

测试ThreadLocal的GC

public class ThreadLocalGC { public static class User { private String name; public User(String name) { this.name = name; } // 监控一下User垃圾回收 @Override protected void finalize() { System.out.println("User GC"); } @Override public String toString() { return "User{" + "name='" + name + '\'' + '}'; } } public static class UserRun implements Runnable { private long sleepTime; public UserRun(long sleepTime) { this.sleepTime = sleepTime; } private static ThreadLocal<User> TL = new ThreadLocal<User>() { // 监控ThreadLocal垃圾回收 @Override protected void finalize() throws Throwable { super.finalize(); System.out.println(this + ": TL GC"); } }; @Override public void run() { User user = new User("Joy Boy"); TL.set(user); // user置为null,释放User,System.gc()会回收,不置null,System.gc()不会回收 // 如果内存紧张不管是否释放都会回收User // user = null; System.out.println(Thread.currentThread().getId() + ": " + TL.get()); System.out.println("TL1: " + TL); TL.remove(); // -Xmx1m最大堆1m 申请一个大内存触发GC,制造内存紧张 byte[] b = new byte[1024 * 500]; // 手动GC,内存紧张的时候不用手动GC也会触发GC // System.gc(); // 这里重新new一个TL会释放上面的TL TL = new ThreadLocal<User>() { @Override protected void finalize() { System.out.println(this + ": TL GC"); } }; System.out.println(Thread.currentThread().getId() + ": " + TL.get()); System.out.println("TL2: " + TL); try { // 休眠一下可以看到GC,让垃圾回收线程执行完,以此监测GC Thread.sleep(sleepTime); } catch (InterruptedException e) { e.printStackTrace(); } } } /** * 设置JVM参数方便测试 * -Xmx1m 设置堆大小1m * -XX:+PrintGCDetails 打印GC详情 * * 打印信息 * [GC (Allocation Failure) 1099K->676K(1536K), 0.0007083 secs] * [Full GC (Ergonomics) 804K->304K(1536K), 0.0060029 secs] * 12: User{name='Joy Boy'} * TL1: com.zx.threads.thread.local.ThreadLocalGC$UserRun$1@21a66f33 * 12: null * TL2: com.zx.threads.thread.local.ThreadLocalGC$UserRun$2@2f618577 * com.zx.threads.thread.local.ThreadLocalGC$UserRun$1@21a66f33: TL GC * User GC */ public static void main(String[] args) throws InterruptedException { new Thread(new UserRun(1000)).start(); } }

为了方便测试运行的堆内存设置1M,Entry的value是User,key是TL。User和TL都重写了finalize(),用于跟踪GC。启个UserRun线程,测试垃圾回收过程。对重点代码作了解释。TL.remove()之后,在Entry释放了TL,释放User。TL重新new了,所以TL1对象被GC了,User也被GC了,如果TL还有引用是不会被GC的,重新new了,TL1没有被引用,才会被GC。一般生产环境中,不会重新new ThreadLocal(),所以就算Entry释放了TL,ThreadLocal也不会被GC。只有value被释放了之后会被GC。 最终Entry也会被GC。因此如果不remove()释放掉Entry,可能会导致OOM。

同一线程每new一个ThreadLocal,生成一个Entry,实现原理

通过阅读ThreadLocal源码,它维护着一个ThreadLocalMap,该Map维护着一个Entry数组。在同一线程中,每new一个ThreadLocal就是一个Entry,根据threadLocalHashCode和数组长度减一,与运算,即threadLocalHashCode & (len - 1)计算出数组下标,插入数组。如果同一线程中创建了多个ThreadLocal,那么生成的下标肯定是不一样的,而数组长度是一定的,默认长度是16,所以threadLocalHashCode肯定是会变化的,才会算出不一样下标。问题是threadLocalHashCode是如何变化的?

为搞清楚这个问题,先看看ThreadLocal的threadLocalHashCode相关源码。

private final int threadLocalHashCode = nextHashCode(); private static AtomicInteger nextHashCode = new AtomicInteger(); private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }

在ThreadLocal中threadLocalHashCode被声明为,私有的final整型基本数据类型,且调用静态方法nextHashCode()赋值。这个final关键字和调用nextHashCode()为其实现了同一线程中,每new一个ThreadLocal就会生成新threadLocalHashCode,final定义的字段(没有static),构造函数完成之前,不能初始化该字段,即构造函数完成之前,该字段对其他线程不可见。所以每次new ThreadLocal()都会初始化一个threadLocalHashCode,这个初始化就是调用nextHashCode();

通过相关源码的字节码指令也可以看出

/** * public <init>()V * L0 * LINENUMBER 5 L0 * ALOAD 0 * INVOKESPECIAL java/lang/Object.<init> ()V * L1 * LINENUMBER 14 L1 * ALOAD 0 * INVOKESTATIC com/zx/finals/FinalByMethod.nextHashCode ()I * PUTFIELD com/zx/finals/FinalByMethod.threadLocalHashCode : I * RETURN * L2 * LOCALVARIABLE this Lcom/zx/finals/FinalByMethod; L0 L2 0 * MAXSTACK = 2 * MAXLOCALS = 1 */

public <init>()V,就是new ThreadLocal()执行的,

INVOKESTATIC com/zx/finals/FinalByMethod.nextHashCode ()I,

就是执行静态方法nextHashCode ()。

PUTFIELD com/zx/finals/FinalByMethod.threadLocalHashCode : I,

赋值给threadLocalHashCode

0x61c88647猜想

ThreadLocal.ThreadLocalMap就是一个散列表,其散列函数通过ThreadLocal的散列码跟散列表长度取模实现,ThreadLocal的散列码是0x61c88647的叠加得出。

ThreadLocal散列叠加数0x61c88647,关于这个数的猜想

在说这个散列叠加数之前,先了解一下黄金分割

数学定义:把一条线段分割为两部分,使较大部分与全长的比值等于较小部分与较大的比值,则这个比值即为黄金分割。其比值是 2 / (√5 + 1) = (√5 - 1) / 2,近似值为0.618,通常用希腊字母 Ф [fi:] 表示这个值。有些文献用 φ (Ф的小写) 表示斐波那契黄金分割数 (√5 - 1) / 2 ≈ 0.618,用 Ф 表示黄金分区率的倒数 1 / φ,(√5 + 1) / 2 ≈ 1.618

A————————C——————B,设一条线段AB的长度为a,C点在靠近B点的黄金分割点上,且AC为b,则b与a的比叫作黄金比

0x61c88647的十进制表示1640531527,假设这个数是a的黄金分割点,也就是C点,以下是关于这个假设的推理:

1.当这个数是AC,求AB

2.当这个数是BC,求AB

如何有效解决ThreadLocal在多线程环境下引发的数据隔离和线程安全问题?


第一种假设,AB:2654435770,其二进制表示10011110001101110111100110111010

第二种假设,AB:4294967297,其二进制表示100000000000000000000000000000001,

在计算机里更倾向于第二种假设,因其Java二进制位移表示(1L << 32) + 1,方便计算加一可以忽略表示为

Integer.MAX_VALUE = 1 << 31 = 2³¹ = -2147483648,转成Long表示,1L << 31 = 2147483648

1L << 32 = 2147483648 * 2 = 4294967296。

逆向证明过程

AB * ((√5 - 1) / 2),其Java代码表示

0x61c88647 = (1L << 32) - 2654435769 = 1640531527;

2654435769其实就是较长的那段AC,1640531527就是较短那段BC。

ThreadLocal的threadLocalHashCode,这个线程本地变量散列码,是通过0x61c88647叠加来起到减少冲突的作用

ThreadLocal的nextHashCode() 就是叠加函数,准确说是扰乱函数,通过叠加0x61c88647来扰乱散列码,以此减少冲突。

ThreadLocalMap散列表通过 threadLocalHashCode & (len - 1) 散列函数寻址,len是散列表长度

threadLocalHashCode & (len - 1) 等同于threadLocalHashCode % len (threadLocalHashCode mod len)

线程本地变量散列码和长度减一与运算,求余数,

(len - 1),len是2的幂,默认是(2⁴ - 1),二进制表示1111,(32 - 4)位高位补0,所以与运用算只算最低4位,threadLocalHashCode最低4位就是余数

threadLocalHashCode的散列是运用乘法散列方案,设 k = 0x61c88647,则散列函数 h(K) = k, 2k, 3k...nk,

ThreadLocalMap是运用除法散列方案 h(K) = nk mod len


关于黄金分割使得散列均匀分布早有证明,这个证明中斐波那契数列起着重要作用,参考Donald E.Knuth The Art of Computer Programming 6.4

如果非要说0x61c88647跟黄金分割有关系,刚好是分割了Integer.MAX_VALUE的两倍的黄金分割点,

测试用AC段2654435769作为这个分割数也能均匀分布散列码。

最终猜测只要这个分割数足够大,那么每次叠加都是唯一散列码,用这个唯一码再去取模散列表长度时,出现冲突概率会非常小,加上散列表的扩容机制,这个冲突概率就更小了。