文章很长,建议收藏起来,慢慢读! 疯狂创客圈为小伙伴奉上以下珍贵的学习资源:
- 疯狂创客圈 经典图书 : 《Netty Zookeeper Redis 高并发实战》 面试必备 + 大厂必备 + 涨薪必备
- 疯狂创客圈 经典图书 : 《SpringCloud、Nginx高并发核心编程》 面试必备 + 大厂必备 + 涨薪必备
- 资源宝库: Java程序员必备 网盘资源大集合 价值>1000元 随便取 GO->【博客园总入口 】
- 独孤九剑:Netty灵魂实验 : 本地 100W连接 高并发实验,瞬间提升Java内力
推荐2:史上最全 Java 面试题 21 个专题
史上最全 Java 面试题 21 个专题 | 阿里、京东、美团、头条.... 随意挑、横着走!!! |
---|---|
1: JVM面试题(史上最强、持续更新、吐血推荐) | https://www.cnblogs.com/crazymakercircle/p/14365820.html |
2:Java基础面试题(史上最全、持续更新、吐血推荐) | https://www.cnblogs.com/crazymakercircle/p/14366081.html |
3:死锁面试题(史上最强、持续更新) | https://www.cnblogs.com/crazymakercircle/p/14323919.html |
4:设计模式面试题 (史上最全、持续更新、吐血推荐) | https://www.cnblogs.com/crazymakercircle/p/14367101.html |
5:架构设计面试题 (史上最全、持续更新、吐血推荐) | https://www.cnblogs.com/crazymakercircle/p/14367907.html |
还有 10 + 篇必刷、必刷 的面试题 | 更多 ....., 请参见【 疯狂创客圈 高并发 总目录 】 |
推荐3: 疯狂创客圈 高质量 博文
springCloud 高质量 博文 | |
---|---|
nacos 实战(史上最全) | sentinel (史上最全+入门教程) |
springcloud + webflux 高并发实战 | Webflux(史上最全) |
SpringCloud gateway (史上最全) | spring security (史上最全) |
还有 10 + 篇 必刷、必刷 的高质量 博文 | 更多 ....., 请参见【 疯狂创客圈 高并发 总目录 】 |
一、ThreadLocal 介绍:
正如 JDK 注释中所说的那样: ThreadLocal 类提供线程局部变量,它通常是私有类中希望将状态与线程关联的静态字段。
简而言之,就是 ThreadLocal 提供了线程间数据隔离的功能,从它的命名上也能知道这是属于一个线程的本地变量。也就是说,每个线程都会在 ThreadLocal 中保存一份该线程独有的数据,所以它是线程安全的。
熟悉 Spring 的同学可能知道 Bean 的作用域(Scope),而 ThreadLocal 的作用域就是线程。
下面通过一个简单示例来展示一下 ThreadLocal 的特性:
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
// 创建一个有2个核心线程数的线程池
ExecutorService threadPool = new ThreadPoolExecutor(2, 2, 1, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(10));
// 线程池提交一个任务,将任务序号及执行该任务的子线程的线程名放到 ThreadLocal 中
threadPool.execute(() -> threadLocal.set("任务1: " + Thread.currentThread().getName()));
threadPool.execute(() -> threadLocal.set("任务2: " + Thread.currentThread().getName()));
threadPool.execute(() -> threadLocal.set("任务3: " + Thread.currentThread().getName()));
// 输出 ThreadLocal 中的内容
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> System.out.println("ThreadLocal value of " + Thread.currentThread().getName() + " = " + threadLocal.get()));
}
// 线程池记得关闭
threadPool.shutdown();
}
上面代码首先创建了一个有2个核心线程数的普通线程池,随后提交一个任务,将任务序号及执行该任务的子线程的线程名放到 ThreadLocal 中,最后在一个 for 循环中输出线程池中各个线程存储在 ThreadLocal 中的值。
这个程序的输出结果是:
ThreadLocal value of pool-1-thread-1 = 任务3: pool-1-thread-1
ThreadLocal value of pool-1-thread-2 = 任务2: pool-1-thread-2
ThreadLocal value of pool-1-thread-1 = 任务3: pool-1-thread-1
ThreadLocal value of pool-1-thread-2 = 任务2: pool-1-thread-2
ThreadLocal value of pool-1-thread-1 = 任务3: pool-1-thread-1
ThreadLocal value of pool-1-thread-2 = 任务2: pool-1-thread-2
ThreadLocal value of pool-1-thread-1 = 任务3: pool-1-thread-1
ThreadLocal value of pool-1-thread-2 = 任务2: pool-1-thread-2
ThreadLocal value of pool-1-thread-1 = 任务3: pool-1-thread-1
ThreadLocal value of pool-1-thread-2 = 任务2: pool-1-thread-2
由此可见,线程池中执行提交的任务的是名为 pool-1-thread-1 的线程,随后多次输出线程池核心线程在 ThreadLocal 变量中存储的的内容也表明:每个线程在 ThreadLocal 中存储的内容是当前线程独有的,在多线程环境下,能够有效防止自己的变量被其他线程修改(存储的内容是同一个引用类型对象的情况除外)。
二、ThreadLocal 实现原理:
在 JDK1.8 版本中 ThreadLocal 类的源码总共723行,去掉注释大概有350行,应该算是 JDK 核心类库中代码量比较少的一个类了,相对来说它的源码还是挺容易理解的。
下面,就从 ThreadLocal 的数据结构开始聊聊它的实现原理吧。
底层数据结构:
ThreadLocal 底层是通过 ThreadLocalMap 这个静态内部类来存储数据的,ThreadLocalMap 就是一个键值对的 Map,它的底层是 Entry 对象数组,Entry 对象中存放的键是 ThreadLocal 对象,值是 Object 类型的具体存储内容。
除此之外,ThreadLocalMap 也是 Thread 类一个属性。
如何证明上面给出的 ThreadLocal 类底层数据结构的正确性?
我们可以从 ThreadLocal#get() 方法开始追踪代码,看看线程局部变量到底是从哪里被取出来的。
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取 Thread 类中 ThreadLocal.ThreadLocalMap 类型的 threadLocals 变量
ThreadLocalMap map = getMap(t);
// 若 threadLocals 变量不为空,根据 ThreadLocal 对象来获取 key 对应的 value
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 若 threadLocals 变量是 NULL,初始化一个新的 ThreadLocalMap 对象
return setInitialValue();
}
// ThreadLocal#setInitialValue
// 初始化一个新的 ThreadLocalMap 对象
private T setInitialValue() {
// 初始化一个 NULL 值
T value = initialValue();
// 获取当前线程
Thread t = Thread.currentThread();
// 获取 Thread 类中 ThreadLocal.ThreadLocalMap 类型的 threadLocals 变量
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
// ThreadLocalMap#createMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
通过 ThreadLocal#get() 方法可以很清晰的看到,我们根据 ThreadLocal 对象从 ThreadLocal 中读取数据时,首先会获取当前线程对象,然后得到当前线程对象中 ThreadLocal.ThreadLocalMap 类型的 threadLocals 属性;
如果 threadLocals 属性不为空,会根据 ThreadLocal 对象作为 key 来获取 key 对应的 value;如果 threadLocals 变量是 NULL,就初始化一个新的ThreadLocalMap 对象。
再看 ThreadLocalMap 的构造方法,也就是 Thread 类中 ThreadLocal.ThreadLocalMap 类型的 threadLocals 属性不为空时的执行逻辑。
// ThreadLocalMap 构造方法
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 对象作为 key,存储的具体内容 Object 对象作为 value,包装成一个 Entry 对象,放到 ThreadLocalMap 类中类型为 Entry 数组的 table 属性中,这样就完成了线程局部变量的存储。
所以说, ThreadLocal 中的数据最终是存放在 ThreadLocalMap 这个类中的 。
散列方式:
在 ThreadLocalMap#set(ThreadLocal<?> key, Object value) 方法中我写了一行注释:
// 获取当前 ThreadLocal 对象的散列值
int i = key.threadLocalHashCode & (len-1);
这行代码得到的值其实是一个 ThreadLocal 对象的散列值,这就是 ThreadLocal 的散列方式,我们称之为 斐波那契散列 。
// ThreadLocal#threadLocalHashCode
private final int threadLocalHashCode = nextHashCode();
// ThreadLocal#nextHashCode
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// ThreadLocal#nextHashCode
private static AtomicInteger nextHashCode = new AtomicInteger();
// AtomicInteger#getAndAdd
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
// 魔数 ThreadLocal#HASH_INCREMENT
private static final int HASH_INCREMENT = 0x61c88647;
key.threadLocalHashCode 所涉及的函数及属性如上所示,每一个 ThreadLocal 的 threadLocalHashCode 属性都是基于魔数 0x61c88647 来生成的。
这里就不讨论选择这个魔数的原因了(其实是我看不太懂),总之大量的实践证明: 使用 0x61c88647 作为魔数生成的 threadLocalHashCode 再与2的幂取余,得到的结果分布很均匀。
注: 对 A 进行2的幂取余操作 A % 2^N 可以通过 A & (2^n-1) 来代替,位运算的效率比取模效率高很多。
如何解决哈希冲突:
我们已经知道 ThreadLocalMap 类的底层数据结构是一个 Entry 类型的数组,但与 HashMap 中的 Node 类数组+链表形式不同的是,Entry 类没有 next 属性来构成链表,所以它是一个单纯的数组。
就算上面所说的 斐波那契散列法 真的能够充分散列,但难免还是可能会发生哈希碰撞,那么问题来了,Entry 数组是如何解决哈希冲突的?
这就需要拿出 ThreadLocal#set(T value) 方法了,而具体处理哈希冲突的逻辑是在 ThreadLocalMap#set(ThreadLocal<?> key, Object value) 方法中的:
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取 Thread 类中 ThreadLocal.ThreadLocalMap 类型的 threadLocals 变量
ThreadLocalMap map = getMap(t);
// 若 threadLocals 变量不为空,进行赋值;否则新建一个 ThreadLocalMap 对象来存储
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
// ThreadLocalMap#set
private void set(ThreadLocal<?> key, Object value) {
// 获取 ThreadLocalMap 的 Entry 数组对象
Entry[] tab = table;
int len = tab.length;
// 基于斐波那契散列法获取当前 ThreadLocal 对象的散列值
int i = key.threadLocalHashCode & (len-1);
// 解决哈希冲突,线性探测法
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 代码(1)
if (k == key) {
e.value = value;
return;
}
// 代码(2)
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 代码(3)将 key-value 包装成 Entry 对象放在数组退出循环时的位置中
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
// ThreadLocalMap#nextIndex
// Entry 数组的下一个索引,若超过数组大小则从0开始,相当于环形数组
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
具体分析处理哈希冲突的 ThreadLocalMap#set(ThreadLocal<?> key, Object value) 方法,可以看到,在拿到 ThreadLocal 对象的散列值之后进入了一个 for 循环,循环的条件也很清楚:从 Entry 数组的 ThreadLocal 对象散列值处开始,每次向后挪一位,如果超过数组大小则从0开始继续遍历,直到 Entry 对象为 NULL 为止。
在循环过程中:
- 如代码(1),如果当前 ThreadLocal 对象正好等于 Entry 对象中的 key 属性,直接更新 ThreadLocal 中 value 的值;
- 如代码(2),如果当前 ThreadLocal 对象不等于 Entry 对象中的 key 属性,并且 Entry 对象的 key 是空的,这里进行的逻辑其实是 设置键值对,同时清理无效的 Entry (一定程序防止内存泄漏,下文会有详细介绍);
- 如代码(3),如果在遍历中没有发现当前 TheadLocal 对象的散列值,也没有发现 Entry 对象的 key 为空的情况,而是满足了退出循环的条件,即 Entry 对象为空时,那么就会创建一个 新的 Entry 对象进行存储 ,同时做一次 启发式清理 ,将 Entry 数组中 key 为空,value 不为空的对象的 value 值释放;
至此,我们分析完了在向 ThreadLocal 中存储数据时,拿到 ThreadLocal 对象散列值之后的逻辑,回到本小节的主题—— ThreadLocal 是如何解决哈希冲突的?
由上面的代码可以知道,在基于斐波那契散列法获取当前 ThreadLocal 对象的散列值之后进入了一个循环,在循环中是处理具体处理哈希冲突的方法:
- 如果散列值已存在且 key 为同一个对象,直接更新 value
- 如果散列值已存在但 key 不是同一个对象,尝试在下一个空的位置进行存储
所以,来总结一下 ThreadLocal 处理哈希冲突的方式就是:如果在 set 时遇到哈希冲突,ThreadLocal 会通过线性探测法尝试在数组下一个索引位置进行存储,同时在 set 过程中 ThreadLocal 会释放 key 为 NULL,value 不为 NULL 的脏 Entry对象的 value 属性来防止内存泄漏 。
初始容量及扩容机制:
在上文中有提到过 ThreadLocalMap 的构造方法,这里详细说明一下。
// ThreadLocalMap 构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化 Entry 数组
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
// 设置扩容条件
setThreshold(INITIAL_CAPACITY);
}
ThreadLocalMap 的初始容量是 16:
// 初始化容量
private static final int INITIAL_CAPACITY = 16;
下面聊一下 ThreadLocalMap 的扩容机制 ,它在扩容前有两个判断的步骤,都满足后才会进行最终扩容:
- ThreadLocalMap#set(ThreadLocal<?> key, Object value) 方法中可能会触发启发式清理,在清理无效 Entry 对象后,如果数组长度大于等于数组定义长度的 2/3,则首先进行 rehash;
// rehash 条件
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
- rehash 会触发一次全量清理,如果数组长度大于等于数组定义长度的 1/2,则进行 resize(扩容);
// 扩容条件
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
- 进行扩容时,Entry 数组为扩容为 原来的2倍 ,重新计算 key 的散列值,如果遇到 key 为 NULL 的情况,会将其 value 也置为 NULL,帮助虚拟机进行GC。
// 具体的扩容函数
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 中存储的是线程的局部变量,那如果现在有个需求,想要实现线程间局部变量传递,这该如何实现呢?
大佬们早已料到会有这样的需求,于是设计出了 InheritableThreadLocal 类。
InheritableThreadLocal 类的源码除去注释之外一共不超过10行,因为它是继承于 ThreadLocal 类,很多东西在 ThreadLocal 类中已经实现了,InheritableThreadLocal 类只重写了其中三个方法:
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
我们先用一个简单的示例来实践一下父子线程间局部变量的传递功能。
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
threadLocal.set("这是父线程设置的值");
new Thread(() -> System.out.println("子线程输出:" + threadLocal.get())).start();
}
// 输出内容
子线程输出:这是父线程设置的值
可以看到,在子线程中通过调用 InheritableThreadLocal#get() 方法,拿到了在父线程中设置的值。
那么,这是如何实现的呢?
实现父子线程间的局部变量共享需要追溯到 Thread 对象的构造方法:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
init(g, target, name, stackSize, null, true);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
// 该参数一般默认是 true
boolean inheritThreadLocals) {
// 省略大部分代码
Thread parent = currentThread();
// 复制父线程的 inheritableThreadLocals 属性,实现父子线程局部变量共享
if (inheritThreadLocals && parent.inheritableThreadLocals != null) {
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
// 省略部分代码
}
在最终执行的构造方法中,有这样一个判断:如果当前父线程(创建子线程的线程)的 inheritableThreadLocals 属性不为 NULL,就会将当下父线程的 inheritableThreadLocals 属性复制给子线程的 inheritableThreadLocals 属性。具体的复制方法如下:
// ThreadLocal#createInheritedMap
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
// 一个个复制父线程 ThreadLocalMap 中的数据
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
// childValue 方法调用的是 InheritableThreadLocal#childValue(T parentValue)
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
需要注意的是,复制父线程共享变量的时机是在创建子线程时,如果在创建子线程后父线程再往 InheritableThreadLocal 类型的对象中设置内容,将不再对子线程可见。
ThreadLocal 内存泄漏分析:
最后再来说说 ThreadLocal 的内存泄漏问题,众所周知,如果使用不当,ThreadLocal 会导致内存泄漏。
内存泄漏 是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
发生内存泄漏的原因:
而 ThreadLocal 发生内存泄漏的原因需要从 Entry 对象说起。
// ThreadLocal->ThreadLocalMap->Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry 对象的 key 即 ThreadLocal 类是继承于 WeakReference 弱引用类。具有弱引用的对象有更短暂的生命周期,在发生 GC 活动时,无论内存空间是否足够,垃圾回收器都会回收具有弱引用的对象。
由于 Entry 对象的 key 是继承于 WeakReference 弱引用类的,若 ThreadLocal 类没有外部强引用,当发生 GC 活动时就会将 ThreadLocal 对象回收。
而此时如果创建 ThreadLocal 类的线程依然活动,那么 Entry 对象中 ThreadLocal 对象对应的 value 就依旧具有强引用而不会被回收,从而导致内存泄漏。
如何解决内存泄漏问题:
要想解决内存泄漏问题其实很简单,只需要记得在使用完 ThreadLocal 中存储的内容后将它 remove 掉就可以了。
这是主动防止发生内存泄漏问题的手段,但其实设计 ThreadLocal 的大神当然也发现了 ThreadLocal 可能引发内存泄漏的问题,所以他们也设计了相应的手段来防止内存泄漏。
ThreadLocal 内部如何防止内存泄漏:
在上文中描述 ThreadLocalMap#set(ThreadLocal key, Object value) 其实已经有涉及 ThreadLocal 内部清理无效 Entry 的逻辑了,在通过线性检测法处理哈希冲突时,若 Entry 数组的 key 与当前 ThreadLocal 不是同一个对象,同时 key 为空的时候,会进行 清理无效 Entry 的处理,即 ThreadLOcalMap#replaceStaleEntry(ThreadLocal key, Object value, int staleSlot) 方法:
- 这个方法中也是一个循环,循环的逻辑与 ThreadLocalMap#set(ThreadLocal<?> key, Object value) 方法一致;
- 在循环过程中如果找到了将要存储的 ThreadLocal 对象,则会将它与进入 replaceStaleEntry 方法时满足条件的 k 值做交换,同时将 value 更新;
- 如果没有找到将要存储的 ThreadLocal 对象,则会在此 k 值处新建一个 Entry 对象存储;
- 同时,在循环过程中如果发现其他无效的 Entry( key 为 NULL,value还在的情况,可能导致内存泄漏,下文会有详细描述),会顺势找到 Entry 数组中所有的无效 Entry,释放这些无效 Entry(通过将 key 和 value 都设置为NULL),在一定程度上避免了内存泄漏;
如果满足线性检测循环结束条件了,即遇到了 Entry==NULL 的情况,就新建一个 Entry 对象来存储数据。然后会进行一次启发式清理,如果启发式清理没有成功释放满足条件的对象,同时满足扩容条件时,会执行 ThreadLocalMap#rehash() 方法。
private void rehash() {
// 全量清理
expungeStaleEntries();
// 满足条件则扩容
if (size >= threshold - threshold / 4)
resize();
}
ThreadLocalMap#rehash() 方法中会对 ThreadLocalMap 进行一次全量清理,全量清理会遍历整个 Entry 数组,删除所有 key 为 NULL,value 不为 NULL 的脏 Entry对象。
// 全量清理
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);
}
}
进行全量清理之后,如果 Entry 数组的大小大于等于 threshold - threshold / 4 ,则会进行2倍扩容。
总结一下:在ThreadLocal 内部是通过在 get、set、remove 方法中主动进行清理 key 为 NULL 且 value 不为 NULL 的无效 Entry 来避免内存泄漏问题。
但是基于 get、set 方法让 ThreadLocal 自行清理无效 Entry 对象并不能完全避免内存泄漏问题,要彻底解决内存泄漏问题还得养成使用完就主动调用 remove 方法释放资源的好习惯。
三、ThreadLocal的常见面试题目
什么是ThreadLocal
ThreadLocal 是 JDK java.lang 包下的一个类,是天然的线程安全的类,
1.ThreadLoca 是线程局部变量,这个变量与普通变量的区别,在于每个访问该变量的线程,在线程内部都会
初始化一个独立的变量副本,只有该线程可以访问【get() or set()】该变量,ThreadLocal实例通常声明
为 private static。
2.线程在存活并且ThreadLocal实例可被访问时,每个线程隐含持有一个线程局部变量副本,当线程生命周期
结束时,ThreadLocal的实例的副本跟着线程一起消失,被GC垃圾回收(除非存在对这些副本的其他引用)
JDK 源码中解析:
/**
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code 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).
* /
稍微翻译一下:ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。
ThreadLocalMap 和HashMap区别
HashMap 的数据结构是数组+链表
ThreadLocalMap的数据结构仅仅是数组
HashMap 是通过链地址法解决hash 冲突的问题
ThreadLocalMap 是通过开放地址法来解决hash 冲突的问题
HashMap 里面的Entry 内部类的引用都是强引用
ThreadLocalMap里面的Entry 内部类中的key 是弱引用,value 是强引用
ThreadLocal怎么用
讨论ThreadLocal用在什么地方前,我们先明确下,如果仅仅就一个线程,那么都不用谈ThreadLocal的,ThreadLocal是用在多线程的场景的!!!
ThreadLocal归纳下来就3类用途:
- 保存线程上下文信息,在任意需要的地方可以获取!!!
- 线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失!!!
- 线程间数据隔离
1.保存线程上下文信息,在任意需要的地方可以获取!!!
由于ThreadLocal的特性,同一线程在某地方进行设置,在随后的任意地方都可以获取到。从而可以用来保存线程上下文信息。
常用的比如每个请求怎么把一串后续关联起来,就可以用ThreadLocal进行set,在后续的任意需要记录日志的方法里面进行get获取到请求id,从而把整个请求串起来。
还有比如Spring的事务管理,用ThreadLocal存储Connection,从而各个DAO可以获取同一Connection,可以进行事务回滚,提交等操作。
2.线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失!!!
由于不需要共享信息,自然就不存在竞争问题了,从而保证了某些情况下线程的安全,以及避免了某些情况需要考虑线程安全必须同步带来的性能损失!!!
ThreadLocal局限性
ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。但是ThreadLocal也有局限性,我们来看看阿里规范:
这类场景阿里规范里面也提到了:
ThreadLocal用法
public class MyThreadLocalDemo {
private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
public static void main(String[] args) throws InterruptedException {
int threads = 9;
MyThreadLocalDemo demo = new MyThreadLocalDemo();
CountDownLatch countDownLatch = new CountDownLatch(threads);
for (int i = 0; i < threads; i++) {
Thread thread = new Thread(() -> {
threadLocal.set(Thread.currentThread().getName());
System.out.println("threadLocal.get()================>" + threadLocal.get());
countDownLatch.countDown();
}, "执行线程 - " + i);
thread.start();
}
countDownLatch.await();
}
}
代码运行结果:
threadLocal.get()================>执行线程 - 1
threadLocal.get()================>执行线程 - 0
threadLocal.get()================>执行线程 - 3
threadLocal.get()================>执行线程 - 4
threadLocal.get()================>执行线程 - 5
threadLocal.get()================>执行线程 - 8
threadLocal.get()================>执行线程 - 7
threadLocal.get()================>执行线程 - 2
threadLocal.get()================>执行线程 - 6
Process finished with exit code 0
ThreadLocal的原理
ThreadLocal虽然叫线程局部变量,但是实际上它并不存放任何的信息,可以这样理解:它是线程(Thread)操作ThreadLocalMap中存放的变量的桥梁。它主要提供了初始化、set()、get()、remove()几个方法。这样说可能有点抽象,下面画个图说明一下在线程中使用ThreadLocal实例的set()和get()方法的简单流程图。
假设我们有如下的代码,主线程的线程名字是main(也有可能不是main):
public class Main {
private static final ThreadLocal<String> LOCAL = new ThreadLocal<>();
public static void main(String[] args) throws Exception{
LOCAL.set("doge");
System.out.println(LOCAL.get());
}
}
上面只描述了单线程的情况并且因为是主线程忽略了Thread t = new Thread()这一步,如果有多个线程会稍微复杂一些,但是原理是不变的,ThreadLocal实例总是通过Thread.currentThread()获取到当前操作线程实例,然后去操作线程实例中的ThreadLocalMap类型的成员变量,因此它是一个桥梁,本身不具备存储功能
链地址法
这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。列如对于关键字集合{12,67,56,16,25,37, 22,29,15,47,48,34},我们用前面同样的12为除数,进行除留余数法:
开放地址法
这种方法的基本思想是一旦发生了冲突,就去寻找下一个空的散列地址(这非常重要,源码都是根据这个特性,必须理解这里才能往下走),只要散列表足够大,空的散列地址总能找到,并将记录存入。
比如说,我们的关键字集合为{12,33,4,5,15,25},表长为10。 我们用散列函数f(key) = key mod l0。 当计算前S个数{12,33,4,5}时,都是没有冲突的散列地址,直接存入(蓝色代表为空的,可以存放数据):
计算key = 15时,发现f(15) = 5,此时就与5所在的位置冲突。于是我们应用上面的公式f(15) = (f(15)+1) mod 10 =6。于是将15存入下标为6的位置。这其实就是房子被人买了于是买下一间的作法:
链地址法和开放地址法的优缺点
开放地址法:
容易产生堆积问题,不适于大规模的数据存储。
散列函数的设计对冲突会有很大的影响,插入时可能会出现多次冲突的现象。
删除的元素是多个冲突元素中的一个,需要对后面的元素作处理,实现较复杂。
链地址法:
处理冲突简单,且无堆积现象,平均查找长度短。
链表中的结点是动态申请的,适合构造表不能确定长度的情况。
删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间。
ThreadLocalMap 采用开放地址法原因
ThreadLocal 中看到一个属性 HASH_INCREMENT = 0x61c88647 ,0x61c88647 是一个神奇的数字,让哈希码能均匀的分布在2的N次方的数组里, 即 Entry[] table
通过HASH_INCREMENT 可以看到,ThreadLocal
中使用了斐波那契散列法,来保证哈希表的离散度。而它选用的乘数值即是2^32 * 黄金分割比
。
什么是散列?
散列(Hash)也称为哈希,就是把任意长度的输入,通过散列算法,变换成固定长度的输出,这个输出值就是散列值。
ThreadLocal 往往存放的数据量不会特别大(而且key 是弱引用又会被垃圾回收,及时让数据量更小),这个时候开放地址法简单的结构会显得更省空间,同时数组的查询效率也是非常高,加上第一点的保障,冲突概率也低.
解决哈希冲突
ThreadLocal中的hash code非常简单,就是调用AtomicInteger的getAndAdd方法,参数是个固定值0x61c88647。
private static AtomicInteger nextHashCode =
new AtomicInteger();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
上面说过ThreadLocalMap的结构非常简单只用一个数组存储,并没有链表结构,当出现Hash冲突时采用线性查找的方式,所谓线性查找,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。如果产生多次hash冲突,处理起来就没有HashMap的效率高,为了避免哈希冲突,使用尽量少的threadlocal变量
内存泄漏问题
在JAVA里面,存在强引用、弱引用、软引用、虚引用。这里主要谈一下强引用和弱引用。
强引用,就不必说了,类似于:
A a = new A();
B b = new B();
考虑这样的情况:
C c = new C(b);
b = null;
考虑下GC的情况。要知道b被置为null,那么是否意味着一段时间后GC工作可以回收b所分配的内存空间呢?答案是否定的,因为即便b被置为null,但是c仍然持有对b的引用,而且还是强引用,所以GC不会回收b原先所分配的空间!既不能回收利用,又不能使用,这就造成了内存泄露。
那么如何处理呢?
可以c = null;也可以使用弱引用!(WeakReference w = new WeakReference(b);)
ThreadLocal使用到了弱引用,是否意味着不会存在内存泄露呢?
把ThreadLocal置为null,那么意味着Heap中的ThreadLocal实例不在有强引用指向,只有弱引用存在,因此GC是可以回收这部分空间的,也就是key是可以回收的。但是value却存在一条从Current Thread过来的强引用链。因此只有当Current Thread销毁时,value才能得到释放。
只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间内不会被回收的,就发生了我们认为的内存泄露。最要命的是线程对象不被回收的情况,比如使用线程池的时候,线程结束是不会销毁的,再次使用的,就可能出现内存泄露。
那么如何有效的避免呢?
在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。我们也可以通过调用ThreadLocal的remove方法进行释放!也就是每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
ThreadLocal使用
ThreadLocal使用的一般步骤:
1、在多线程的类(如ThreadDemo类)中。创建一个ThreadLocal对象threadXxx,用来保存线程间须要隔离处理的对象xxx。
2、在ThreadDemo类中。创建一个获取要隔离访问的数据的方法getXxx(),在方法中推断,若ThreadLocal对象为null时候,应该new()一个隔离訪问类型的对象,并强制转换为要应用的类型。
3、在ThreadDemo类的run()方法中。通过getXxx()方法获取要操作的数据。这样能够保证每一个线程相应一个数据对象,在不论什么时刻都操作的是这个对象。
使用示例:
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
threadLocal.set(i);
System.out.println(Thread.currentThread().getName() + " = " + threadLocal.get());
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
threadLocal.remove();
}
}, "threadLocal test 1").start();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " = " + threadLocal.get());
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
threadLocal.remove();
}
}, "threadLocal test 2").start();
}
输出
threadLocal test 1 = 0
threadLocal test 2 = null
threadLocal test 2 = null
threadLocal test 1 = 1
threadLocal test 2 = null
threadLocal test 1 = 2
threadLocal test 2 = null
threadLocal test 1 = 3
threadLocal test 2 = null
threadLocal test 1 = 4
threadLocal test 2 = null
threadLocal test 1 = 5
threadLocal test 2 = null
threadLocal test 1 = 6
threadLocal test 2 = null
threadLocal test 1 = 7
threadLocal test 2 = null
threadLocal test 1 = 8
threadLocal test 2 = null
threadLocal test 1 = 9
与Synchonized的对照:
ThreadLocal和Synchonized都用于解决多线程并发访问。可是ThreadLocal与synchronized有本质的差别。synchronized是利用锁的机制,使变量或代码块在某一时该仅仅能被一个线程访问。而ThreadLocal为每个线程都提供了变量的副本,使得每个线程在某一时间访问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时可以获得数据共享。
Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
线程隔离特性
线程隔离特性,只有在线程内才能获取到对应的值,线程外不能访问。
(1)Synchronized是通过线程等待,牺牲时间来解决访问冲突
(1)ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突
需要了解ThreadLocal的源码解析: 点此了解
四、ThreadLocal源码分析
从Thread源码入手:
public class Thread implements Runnable {
......
//与此线程有关的ThreadLocal值。该映射由ThreadLocal类维护。
ThreadLocal.ThreadLocalMap threadLocals = null;
//与此线程有关的InheritableThreadLocal值。该Map由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
......
}
从上面Thread类源代码可以看出Thread类中有一个threadLocals和一个inheritableThreadLocals 变量,它们都是ThreadLocalMap类型的变量,默认情况下这两个变量都是null,只有当前线程调用ThreadLocal类的Iset或get方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的get()、set()方法。
1.ThreadLocal的内部属性
ThreadLocalMap 的 key 是 ThreadLocal,但它不会传统的调用 ThreadLocal 的 hashCode 方法(继承自Object 的 hashCode),而是调用 nextHashCode() ,具体运算如下:
public class ThreadLocal<T> {
//获取下一个ThreadLocal实例的哈希魔数
private final int threadLocalHashCode = nextHashCode();
//原子计数器,主要到它被定义为静态
private static AtomicInteger nextHashCode = new AtomicInteger();
//哈希魔数(增长数),也是带符号的32位整型值黄金分割值的取正
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是唯一的。
这里写个demo看一下基于魔数 1640531527 方式产生的hash分布多均匀:
public class ThreadLocalTest {
public static void main(String[] args) {
printAllSlot(8);
printAllSlot(16);
printAllSlot(32);
}
static void printAllSlot(int len) {
System.out.println("********** len = " + len + " ************");
for (int i = 1; i <= 64; i++) {
ThreadLocal<String> t = new ThreadLocal<>();
int slot = getSlot(t, len);
System.out.print(slot + " ");
if (i % len == 0) {
System.out.println(); // 分组换行
}
}
}
/**
* 获取槽位
*
* @param t ThreadLocal
* @param len 模拟map的table的length
* @throws Exception
*/
static int getSlot(ThreadLocal<?> t, int len) {
int hash = getHashCode(t);
return hash & (len - 1);
}
/**
* 反射获取 threadLocalHashCode 字段,因为其为private的
*/
static int getHashCode(ThreadLocal<?> t) {
Field field;
try {
field = t.getClass().getDeclaredField("threadLocalHashCode");
field.setAccessible(true);
return (int) field.get(t);
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
}
上述代码模拟了 ThreadLocal 做为 key 的hashCode产生,看看完美槽位分配:
********** len = 8 ************
2 1 0 7 6 5 4 3
2 1 0 7 6 5 4 3
2 1 0 7 6 5 4 3
2 1 0 7 6 5 4 3
2 1 0 7 6 5 4 3
2 1 0 7 6 5 4 3
2 1 0 7 6 5 4 3
2 1 0 7 6 5 4 3
********** len = 16 ************
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3
********** len = 32 ************
10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0 7 14 21 28 3
10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0 7 14 21 28 3
Process finished with exit code 0
2. ThreadLocal 之 set() 方法
ThreadLocal中set()方法的源码如下:
protected T initialValue() {
return null;
}
/**
* 将此线程局部变量的当前线程副本设置为指定值。大多数子类将不需要
* 重写此方法,而仅依靠{@link #initialValue}
* 方法来设置线程局部变量的值。
*
* @param value 要存储在此线程的thread-local副本中的值
*/
public void set(T value) {
//设置值前总是获取当前线程实例
Thread t = Thread.currentThread();
//从当前线程实例中获取threadLocals属性
ThreadLocalMap map = getMap(t);
if (map != null)
//threadLocals属性不为null则覆盖key为当前的ThreadLocal实例,值为value
map.set(this, value);
else
//threadLocals属性为null,则创建ThreadLocalMap,第一个项的Key为当前的ThreadLocal实例,值为value
createMap(t, value);
}
//这里看到获取ThreadLocalMap实例时候总是从线程实例的成员变量获取
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//创建ThreadLocalMap实例的时候,会把新实例赋值到线程实例的threadLocals成员
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
上面的过程源码很简单,设置值的时候总是先获取当前线程实例并且操作它的变量threadLocals。步骤是:
- 获取当前运行线程的实例。
- 通过线程实例获取线程实例成员threadLocals(ThreadLocalMap),如果为null,则创建一个新的ThreadLocalMap实例赋值到threadLocals。
- 通过threadLocals设置值value,如果原来的哈希槽已经存在值,则进行覆盖。
3.ThreadLocal 之 get() 方法
ThreadLocal中get()方法的源码如下:
/**
* 返回此线程局部变量的当前线程副本中的值。如果该变量没有当前线程的值,
* 则首先通过调用{@link #initialValue}方法将其初始化为*返回的值。
*
* @return 当前线程局部变量中的值
*/
public T get() {
//获取当前线程的实例
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
//根据当前的ThreadLocal实例获取ThreadLocalMap中的Entry,使用的是ThreadLocalMap的getEntry方法
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T) e.value;
return result;
}
}
//线程实例中的threadLocals为null,则调用initialValue方法,并且创建ThreadLocalMap赋值到threadLocals
return setInitialValue();
}
private T setInitialValue() {
// 调用initialValue方法获取值
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// ThreadLocalMap如果未初始化则进行一次创建,已初始化则直接设置值
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
initialValue()方法默认返回null,如果ThreadLocal实例没有使用过set()方法直接使用get()方法,那么ThreadLocalMap中的此ThreadLocal为Key的项会把值设置为initialValue()方法的返回值。如果想改变这个逻辑可以对initialValue()方法进行覆盖。
4.TreadLocal的remove方法
ThreadLocal中remove()方法的源码如下:
public void remove() {
//获取Thread实例中的ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//根据当前ThreadLocal作为Key对ThreadLocalMap的元素进行移除
m.remove(this);
}
这里罗列了 ThreadLocal 的几个public方法,其实所有工作最终都落到了 ThreadLocalMap 的头上,ThreadLocal 仅仅是从当前线程取到 ThreadLocalMap 而已,具体执行,请看下面对 ThreadLocalMap 的分析。
5.内部类ThreadLocalMap的基本结构和源码分析
ThreadLocalMap 是ThreadLocal 内部的一个Map实现,然而它并没有实现任何集合的接口规范,因为它仅供内部使用,数据结构采用 数组 + 开方地址法,Entry 继承 WeakReference,是基于 ThreadLocal 这种特殊场景实现的 Map,它的实现方式很值得研究。
ThreadLocal内部类ThreadLocalMap使用了默认修饰符,也就是包(包私有)可访问的。ThreadLocalMap内部定义了一个静态类Entry。我们重点看下ThreadLocalMap的源码,
5.1先看成员和结构部分
/**
* ThreadLocalMap是一个定制的散列映射,仅适用于维护线程本地变量。
* 它的所有方法都是定义在ThreadLocal类之内。
* 它是包私有的,所以在Thread类中可以定义ThreadLocalMap作为变量。
* 为了处理非常大(指的是值)和长时间的用途,哈希表的Key使用了弱引用(WeakReferences)。
* 引用的队列(弱引用)不再被使用的时候,对应的过期的条目就能通过主动删除移出哈希表。
*/
static class ThreadLocalMap {
//注意这里的Entry的Key为WeakReference<ThreadLocal<?>>
static class Entry extends WeakReference<ThreadLocal<?>> {
//这个是真正的存放的值
Object value;
// Entry的Key就是ThreadLocal实例本身,Value就是输入的值
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//初始化容量,必须是2的幂次方
private static final int INITIAL_CAPACITY = 16;
//哈希(Entry)表,必须时扩容,长度必须为2的幂次方
private Entry[] table;
//哈希表中元素(Entry)的个数
private int size = 0;
//下一次需要扩容的阈值,默认值为0
private int threshold;
//设置下一次需要扩容的阈值,设置值为输入值len的三分之二
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
// 以len为模增加i
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
// 以len为模减少i
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
}
- 这里注意到十分重要的一点:ThreadLocalMap$Entry是WeakReference(弱引用),并且键值Key为ThreadLocal<?>实例本身,这里使用了无限定的泛型通配符。
- ThreadLocalMap 的 key 是 ThreadLocal,但它不会传统的调用 ThreadLocal 的 hashCode 方法(继承自Object 的 hashCode),而是调用 nextHashCode()
5.2接着看ThreadLocalMap的构造函数
// 构造ThreadLocal时候使用,对应ThreadLocal的实例方法void createMap(Thread t, T firstValue)
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 哈希表默认容量为16
table = new Entry[INITIAL_CAPACITY];
// 计算第一个元素的哈希码
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
// 构造InheritableThreadLocal时候使用,基于父线程的ThreadLocalMap里面的内容进行
// 提取放入新的ThreadLocalMap的哈希表中
// 对应ThreadLocal的静态方法static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap)
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
// 基于父ThreadLocalMap的哈希表进行拷贝
for (Entry e : parentTable) {
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
这里注意一下,ThreadLocal的set()方法调用的时候会懒初始化一个ThreadLocalMap并且放入第一个元素。而ThreadLocalMap的私有构造是提供给静态方法ThreadLocal#createInheritedMap()使用的。
5.3ThreadLocalMap 之 set() 方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); // 用key的hashCode计算槽位
// hash冲突时,使用开放地址法
// 因为独特和hash算法,导致hash冲突很少,一般不会走进这个for循环
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) { // key 相同,则覆盖value
e.value = value;
return;
}
if (k == null) { // key = null,说明 key 已经被回收了,进入替换方法
replaceStaleEntry(key, value, i);
return;
}
}
// 新增 Entry
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold) // 清除一些过期的值,并判断是否需要扩容
rehash(); // 扩容
}
这个 set 方法涵盖了很多关键点:
- 开放地址法:与我们常用的Map不同,java里大部分Map都是用链表发解决hash冲突的,而 ThreadLocalMap 采用的是开发地址法。
- hash算法:hash值算法的精妙之处上面已经讲了,均匀的 hash 算法使其可以很好的配合开方地址法使用;
- 过期值清理
下面对 set 方法里面的几个关键方法展开:
1.replaceStaleEntry()
因为开发地址发的使用,导致 replaceStaleEntry 这个方法有些复杂,它的清理工作会涉及到slot前后的非null的slot。
//这里个方法比较长,作用是替换哈希码为staleSlot的哈希槽中Entry的值
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;
// 找到 key 或者 直到 遇到null 的slot 才终止循环
// 遍历staleSlot之后的哈希槽,如果Key匹配则用输入值替换
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如果找到了key,那么需要将它与过期的 slot 交换来维护哈希表的顺序。
// 然后可以将新过期的 slot 或其上面遇到的任何其他过期的 slot
// 给 expungeStaleEntry 以清除或 rehash 这个 run 中的所有其他entries。
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 如果存在,则开始清除前面过期的entry
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 如果我们没有在向前扫描中找到过期的条目,
// 那么在扫描 key 时看到的第一个过期 entry 是仍然存在于 run 中的条目。
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 如果没有找到 key,那么在 slot 中创建新entry
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 如果还有其他过期的entries存在 run 中,则清除他们
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
上文中的 run 不好翻译,理解为开放地址中一个slot中前后不为null的连续entry
2.cleanSomeSlots()
cleanSomeSlots 清除一些slot(一些?是不是有点模糊,到底是哪些?)
//清理第i个哈希槽之后的n个哈希槽,如果遍历的时候发现Entry的Key为null,则n会重置为哈希表的长度,
//expungeStaleEntry有可能会重哈希使得哈希表长度发生变化
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); // n = n / 2, 对数控制循环
return removed;
}
当新元素被添加时,或者另一个过期元素已被删除时,会调用cleanSomeSlots。该方法会试探性地扫描一些 entry 寻找过期的条目。它执行 对数 数量的扫描,是一种 基于不扫描(快速但保留垃圾)和 所有元素扫描之间的平衡。
上面说到的对数数量是多少?循环次数 = log2(N) (log以2为底N的对数),此处N是map的size,如:
log2(4) = 2
log2(5) = 2
log2(18) = 4
因此,此方法并没有真正的清除,只是找到了要清除的位置,而真正的清除在 expungeStaleEntry(int staleSlot) 里面
3.expungeStaleEntry(int staleSlot)
这里是真正的清除,并且不要被方法名迷惑,不仅仅会清除当前过期的slot,还回往后查找直到遇到null的slot为止。开放地址法的清除也较难理解,清除当前slot后还有往后进行rehash。
//对当前哈希表中所有的Key为null的Entry调用expungeStaleEntry
// 1.清空staleSlot对应哈希槽的Key和Value
// 2.对staleSlot到下一个空的哈希槽之间的所有可能冲突的哈希表部分槽进行重哈希,置空Key为null的槽
// 3.注意返回值是staleSlot之后的下一个空的哈希槽的哈希码
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 清空staleSlot对应哈希槽的Key和Value
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash 直到 null 的 slot
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {//空key直接清除
e.value = null;
tab[i] = null;
size--;
} else {//key非空,则Rehash
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
5.4ThreadLocalMap 之 getEntry() 方法
getEntry() 主要是在 ThreadLocal 的 get() 方法里被调用
/**
* 这个方法主要给`ThreadLocal#get()`调用,通过当前ThreadLocal实例获取哈希表中对应的Entry
*
*/
private Entry getEntry(ThreadLocal<?> key) {
// 计算Entry的哈希值
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)//无hash冲突情况
return e;
else // 注意这里,如果e为null或者Key对不上,表示:有hash冲突情况,会调用getEntryAfterMiss
return getEntryAfterMiss(key, i, e);
}
// 如果Key在哈希表中找不到哈希槽的时候会调用此方法
// 这个方法是在遇到 hash 冲突时往后继续查找,并且会清除查找路上遇到的过期slot。
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 这里会通过nextIndex尝试遍历整个哈希表,如果找到匹配的Key则返回Entry
// 如果哈希表中存在Key == null的情况,调用expungeStaleEntry进行清理
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
5.5ThreadLocalMap 之 rehash() 方法
// 重哈希,必要时进行扩容
private void rehash() {
// 清理所有空的哈希槽,并且进行重哈希
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
// 在上面的清除过程中,size会减小,在此处重新计算是否需要扩容
// 并没有直接使用threshold,而是用较低的threshold (约 threshold 的 3/4)提前触发resize
if (size >= threshold - threshold / 4)
resize();
}
// 对当前哈希表中所有的Key为null的Entry调用expungeStaleEntry
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);
}
}
// 扩容,简单的扩大2倍的容量
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (Entry e : oldTab) {
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;
}
PS :ThreadLocalMap 没有 影响因子 的字段,是采用直接设置 threshold 的方式,threshold = len * 2 / 3,相当于不可修改的影响因子为 2/3,比 HashMap 的默认 0.75 要低。这也是减少hash冲突的方式。
5.6ThreadLocalMap 之 remove(key) 方法
/**
* Remove the entry for key.
*/
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;
}
}
}
remove 方法是删除特定的 ThreadLocal,建议在 ThreadLocal 使用完后一定要执行此方法。
五、什么情况下ThreadLocal的使用会导致内存泄漏
其实ThreadLocal本身不存放任何的数据,而ThreadLocal中的数据实际上是存放在线程实例中,从实际来看是线程内存泄漏,底层来看是Thread对象中的成员变量threadLocals持有大量的K-V结构,并且线程一直处于活跃状态导致变量threadLocals无法释放被回收。threadLocals持有大量的K-V结构这一点的前提是要存在大量的ThreadLocal实例的定义,一般来说,一个应用不可能定义大量的ThreadLocal,所以一般的泄漏源是线程一直处于活跃状态导致变量threadLocals无法释放被回收。但是我们知道,·ThreadLocalMap·中的Entry结构的Key用到了弱引用(·WeakReference<ThreadLocal<?>>·),当没有强引用来引用ThreadLocal实例的时候,JVM的GC会回收ThreadLocalMap中的这些Key,此时,ThreadLocalMap中会出现一些Key为null,但是Value不为null的Entry项,这些Entry项如果不主动清理,就会一直驻留在ThreadLocalMap中。也就是为什么ThreadLocal中get()、set()、remove()这些方法中都存在清理ThreadLocalMap实例key为null的代码块。总结下来,内存泄漏可能出现的地方是:
大量地(静态)初始化ThreadLocal实例,初始化之后不再调用get()、set()、remove()方法。
初始化了大量的ThreadLocal,这些ThreadLocal中存放了容量大的Value,并且使用了这些ThreadLocal实例的线程一直处于活跃的状态。
ThreadLocal中一个设计亮点是ThreadLocalMap中的Entry结构的Key用到了弱引用。试想如果使用强引用,等于ThreadLocalMap中的所有数据都是与Thread的生命周期绑定,这样很容易出现因为大量线程持续活跃导致的内存泄漏。使用了弱引用的话,JVM触发GC回收弱引用后,ThreadLocal在下一次调用get()、set()、remove()方法就可以删除那些ThreadLocalMap中Key为null的值,起到了惰性删除释放内存的作用。
其实ThreadLocal在设置内部类ThreadLocal.ThreadLocalMap中构建的Entry哈希表已经考虑到内存泄漏的问题,所以ThreadLocal.ThreadLocalMap$Entry类设计为弱引用,类签名为static class Entry extends WeakReference<ThreadLocal<?>>。之前一篇文章介绍过,如果弱引用关联的对象如果置为null,那么该弱引用会在下一次GC时候回收弱引用关联的对象。举个例子:
public class ThreadLocalMain {
private static ThreadLocal<Integer> TL_1 = new ThreadLocal<>();
public static void main(String[] args) throws Exception {
TL_1.set(1);
TL_1 = null;
System.gc();
Thread.sleep(300);
}
}
这种情况下,TL_1这个ThreadLocal在主动GC之后,线程绑定的ThreadLocal.ThreadLocalMap实例中的Entry哈希表中原来的TL_1所在的哈希槽Entry的引用持有值referent(继承自WeakReference)会变成null,但是Entry中的value是强引用,还存放着TL_1这个ThreadLocal未回收之前的值。这些被”孤立”的哈希槽Entry就是前面说到的要惰性删除的哈希槽。
六、ThreadLocal的最佳实践
其实ThreadLocal的最佳实践很简单:
- 每次使用完ThreadLocal实例,都调用它的remove()方法,清除Entry中的数据。
调用remove()方法最佳时机是线程运行结束之前的finally代码块中调用,这样能完全避免操作不当导致的内存泄漏,这种主动清理的方式比惰性删除有效。
七、黄金分割 - 魔数0x61c88647
1.黄金分割数与斐波那契数列
首先复习一下斐波那契数列,下面的推导过程来自某搜索引擎的wiki:
斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …
通项公式:假设F(n)为该数列的第n项(n ∈ N*),那么这句话可以写成如下形式:F(n) = F(n-1) + F(n-2)。
有趣的是,这样一个完全是自然数的数列,通项公式却是用无理数来表达的。而且当n趋向于无穷大时,前一项与后一项的比值越来越逼近0.618(或者说后一项与前一项的比值小数部分越来越逼近0.618),而这个值0.618就被称为黄金分割数。证明过程如下:
黄金分割数的准确值为(根号5 - 1)/2,约等于0.618。
2.黄金分割数的应用
黄金分割数被广泛使用在美术、摄影等艺术领域,因为它具有严格的比例性、艺术性、和谐性,蕴藏着丰富的美学价值,能够激发人的美感。当然,这些不是本文研究的方向,我们先尝试求出无符号整型和带符号整型的黄金分割数的具体值:
public static void main(String[] args) throws Exception {
//黄金分割数 * 2的32次方 = 2654435769 - 这个是无符号32位整数的黄金分割数对应的那个值
long c = (long) ((1L << 32) * (Math.sqrt(5) - 1) / 2);
System.out.println(c);
//强制转换为带符号为的32位整型,值为-1640531527
int i = (int) c;
System.out.println(i);
}
通过一个线段图理解一下:
也就是2654435769为32位无符号整数的黄金分割值,而-1640531527就是32位带符号整数的黄金分割值。而ThreadLocal中的哈希魔数正是1640531527(十六进制为0x61c88647)。为什么要使用0x61c88647作为哈希魔数?这里提前说一下ThreadLocal在ThreadLocalMap(ThreadLocal在ThreadLocalMap以Key的形式存在)中的哈希求Key下标的规则:
哈希算法:keyIndex = ((i + 1) * HASH_INCREMENT) & (length - 1)
其中,i为ThreadLocal实例的个数,这里的HASH_INCREMENT就是哈希魔数0x61c88647,length为ThreadLocalMap中可容纳的Entry(K-V结构)的个数(或者称为容量)。在ThreadLocal中的内部类ThreadLocalMap的初始化容量为16,扩容后总是2的幂次方,因此我们可以写个Demo模拟整个哈希的过程:
public class Main {
private static final int HASH_INCREMENT = 0x61c88647;
public static void main(String[] args) throws Exception {
hashCode(4);
hashCode(16);
hashCode(32);
}
private static void hashCode(int capacity) throws Exception {
int keyIndex;
for (int i = 0; i < capacity; i++) {
keyIndex = ((i + 1) * HASH_INCREMENT) & (capacity - 1);
System.out.print(keyIndex);
System.out.print(" ");
}
System.out.println();
}
}
上面的例子中,我们分别模拟了ThreadLocalMap容量为4,16,32的情况下,不触发扩容,并且分别”放入”4,16,32个元素到容器中,输出结果如下:
3 2 1 0
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0
7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0
每组的元素经过散列算法后恰好填充满了整个容器,也就是实现了完美散列。实际上,这个并不是偶然,其实整个哈希算法可以转换为多项式证明:证明(x - y) HASH_INCREMENT != 2^n (n m),在x != y,n != m,HASH_INCREMENT为奇数的情况下恒成立,具体证明可以自行完成。HASH_INCREMENT赋值为0x61c88647的API文档注释如下:
连续生成的哈希码之间的差异(增量值),将隐式顺序线程本地id转换为几乎最佳分布的乘法哈希值,这些不同的
哈希值最终生成一个2的幂次方的哈希表。