在多线程编程中,我们经常面临共享数据的问题,而这可能引发一系列并发性和线程安全性的挑战。
Java 提供了许多机制来处理这些问题,比如控制并发的各种锁, 控制线程串行地修改资源, 避免线程安全, 或者通过关键字 volatile 修饰变量, 保证可见性等。
而在解决并发共享的方式中, 还有一种方式, 那么就是线程隔离, 每个线程各自维护资源的副本, 从而避免了共享资源的竞争。
而实现这个实现的一个经典代表就是 ThreadLocal, ThreadLocal 为每个线程提供了独立的变量副本, 从而在不同线程之间隔离数据。这为我们提供了一种简单而强大的方式来确保线程间数据的安全性。
ThreadLocal 的特点:
- 线程隔离性: 每个线程都有自己独立的 ThreadLocal 变量, 彼此之间不共享数据
- 数据共享: ThreadLocal 变量在同一个线程内共享,这意味着在同一线程中的不同方法可以轻松地访问相同的 ThreadLocal 变量
- 线程安全性: 单个 ThreadLocal 实例通常是线程安全的,因为每个线程操作的是自己的副本,而不是共享的实例
- 内存泄漏风险: 因为一个变量, 每个线程都会以副本的形式存储在自己身上, 这本身就是一种消耗空间的行为, 同时它的生命周期和 Thread 一样长, 在使用过程中不手动进行删除, 可能会导致内存泄露
public class Demo {
public static void main(String[] args) {
ThreadLocal<Integer> numThreadLocal = new ThreadLocal<>();
ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
numThreadLocal.set(1);
stringThreadLocal.set("hello");
// 1
System.out.println(numThreadLocal.get());
// 2
System.out.println(stringThreadLocal.get());
}
}
假设现在我们执行上面代码的的线程名为 Thread-1。
程序启动后, 创建了 2 个 ThreadLocal
- numThreadLocal: 存储 Integer 类型的数据
- stringThreadLocal: 存储 String 类型的数据
然后 Thread-1 将 1 存入 numThreadLocal, 将 “hello” 存入 stringThreadLocal
最后 Thread-1 通过 get 方法分别从 numThreadLocal 和 stringThreadLocal 中获取到了 1 和 “hello”。
看起来的我们的数据都是存储在 ThreadLocal 中的。
但是实际中, ThreadLocal 在整个过程中发挥的作用不是存储数据, 而是一个转换器的效果, 真正的数据还是存储在 Thread 中的, 存储的地方是一个 ThreadLocalMap, 可以理解为一个 Map。
每创建一个 ThreadLocal 都分配给自己的唯一编码, 调用这个 ThreadLocal 的 get, set 等方法, ThreadLocal 都是通过这个 code 定位到调用的线程的内部的一个 ThreadLocalMap 的某个位置, 然后给这个 Map 添加或删除数据。
不同的 ThreadLocal 有不同的 code, 所以通过这个 code 定位到 Thread 的 ThreadLocalMap 的某个位置, 从而实现了线程隔离 (实际这个通过 code 定位到具体的位置涉及到 hash, 所以还是有冲突的可能, 但是通过别的方式解决了)。
我们先通过它的几个核心方法简单了解一下。
public class ThreadLocal<T> {
public void set(T value) {
// 获取当前的线程
Thread t = Thread.currentThread();
// 通过当前线程实例获取到 ThreadLocalMap 对象
ThreadLocalMap map = getMap(t);
if (map != null)
// 如果 map 不为 null, 则以当前 threadLocal 实例为 key, 值为 value 进行存入
// ThreadLocalMap 内部的 set 会通过 this, 得到当前的 ThreadLocal, 然后获取里面的 code
map.set(this, value);
else
// map 为 null, 则新建 ThreadLocalMap 并存入 value
createMap(t, value);
}
/**
* 返回指定线程自身的 ThreadLocalMap 对象
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* 为指定的线程创建一个 ThreadLocalMap 对象
*/
void createMap(Thread t, T firstValue) {
// 这里同 ThreadLocalMap 的 set, 会通过 this, 得到当前的 ThreadLocal, 然后获取里面的 code
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
}
从上面的代码, 我们可以知道
ThreadLocal 实际的数据是存在每个线程自身的 ThreadLocalMap 对象中。
那么 ThreadLocalMap 是什么样的呢?
首先 ThreadLocalMap 是定义在 Thread 身上的一个属性。
public class Thread {
// 每个线程内部都有一个 ThreadLocalMap 属性
ThreadLocal.ThreadLocalMap threadLocals = null;
}
我们可以在类 A 中声明一个 TheadLocal, 然后在类 B 中用声明一个 TheadLocal。
同一个线程同时使用到了类 A 和类 B, 那么线程身上的 TheadLocalMap threadLocals 需要支持同时存储 2 个数据以上, 那么 ThreadLocalMap
应该是什么样的一个数据结构呢?
答案是数组, 最终实现出来的结果类似于 HashMap, value 是我们存入的内容, 而 key 就是每个类上的 ThreadLocal 的实例内部的一个 code。
通过这个 TheadLocal 的 code 得到一个 hash 值, 通过这个 hash 值计算得到其在数组中的哪个位置。
这时候如果有另外一个 TheadLocal 实例, 他的 code 不一样, 计算出来的 hash 不一样, 那么这个 TheadLocal 的内容存在数组的另外一个位置。
TheadLocalMap 的理解可以简单的先按照上面的方式, 后面会具体的分析。
向 ThreadLocal 放数据, 也就是 set 过程
- 获取当前线程的所维护的 threadLocalMap
- 若 threadLocalMap 不为 null, 则以当前的 ThreadLocal 实例内部的 code 为 key, 值为 value 的键值对存入 threadLocalMap
- 若 threadLocalMap 为 null 的话, 就为当前的线程新建一个 ThreadLocalMap, 然后在以当前的 ThreadLocal 实例内部的 code 为 key, 值为 value 的键值对存入即可
public class ThreadLocal<T> {
public T get() {
// 1. 获取当前的线程
Thread t = Thread.currentThread();
// 2. 获取当前线程的 ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 3. 当前线程的 ThreadLocalMap 不为空
if (map != null) {
// 获取 map 中 key 为当前 ThreadLocal 内部 code 的 Entry
ThreadLocalMap.Entry e = map.getEntry(this);
// 获取到了需要的 Entry
if (e != null) {
T result = (T)e.value;
return result;
}
}
//若 map 为 null 或者 entry 为 null 的话通过该方法初始化, 并返回该方法返回的 value
return setInitialValue();
}
/**
* 为当前的线程设置一个 value 为 null 的 ThreadLocalMap
*/
private T setInitialValue() {
// 获取一个 null 的 value
T value = initialValue();
Thread t = Thread.currentThread();
// 获取当前线程的 TheadLocalMap
ThreadLocalMap map = getMap(t);
// 不为 null, 设值
if (map != null)
map.set(this, value);
else
// 为 null, 创建一个
createMap(t, value);
return value;
}
/**
* 默认返回一个 null
*/
protected T initialValue() {
return null;
}
}
从 ThreadLocal 取数据, 也就是 get 的过程
- 获取当前线程的所维护的 threadLocalMap
- 若 threadLocalMap 不为 null, 通过当前 ThreadLocal 的 code 为 key 从 threadLocalMap 中获取存储数据的 Entry, Entry 不为 null, 就是找到需要的值了, 返回
- threadLocalMap 为 null, 或者从 threadLocalMap 获取到的 Entry 为空, 声明一个初始的 value 默认为 null, 走一遍 set 的逻辑
- 返回声明的初始 value, 也就是 null
public class ThreadLocal<T> {
public void remove() {
// 获取当前线程的 TheadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
// 从 map 中删除以当前 threadLocal 实例内部 code 为 key 的键值对
// ThreadLocalMap 的 remove 后面在讲
m.remove(this);
}
}
remove 的方法很简单的, 就是从从当前线程获取到 ThreadLocalMap, 不为 null 的话, 调用 ThreadLocalMap 的 remove 进行移除数据。
从上面的分析我们已经知道, 数据其实都放在了线程自身的 ThreadLocalMap 中, ThreadLocal 的 get, set 和 remove 方法实际上是通过 ThreadLocalMap
的 getEntry, set 和 remove 方法实现的。如果想真正全方位的弄懂 ThreadLocal, 就是在了解 TheadLocalMap。
ThreadLocalMap 是 ThreadLocal 一个静态内部类, 和大多数容器一样内部维护了一个数组, 同样的 ThreadLocalMap 内部维护了一个 Entry 类型的 table 数组
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry 是一个以 ThreadLocal 内部 code 为 key, value 为 Object类型 的键值对。 有点类似于 HashMap, T key, Object value。 key 的变量声明在父级 WeakReference 中。
Entry 继承了 WeakReference, 同时在构造函数中将当前的 ThreadLocal 作为参数传递给了父级, 在父级中, 会将对应的 TheadLocal 封装为一个弱引用。
而不像我们平时直接在 Entry 中多声明一个 T key, 然后将 TheadLocal> k 赋值给 key, 这种做法是强引用, key 和当前的 Entry 是共生的, 一起存在一次消亡。
而使用弱引用进行包装可以达到: 在 GC 时, 在 Entry 存在的情况下, k 能被回收。
Java 中的 4 种引用可以看下面的附录, 有简单的介绍。
Thead, TheadLocal, TheadLocalMap 三者的关系如下:
实线表示强引用, 虚线表示弱引用。
每个线程实例中可以通过 Thead.threadLocals 获取到 ThreadLocalMap, 而 ThreadLocalMap 实际上就是一个以 ThreadLocal 实例内部 code 为 key, 任意对象为 value 的 Entry 数组。
当我们为 ThreadLocal 变量赋值时, 就是给对应的线程的 ThreadLocalMap 中放入一个当前 ThreadLocal 实例内部 code 为 key, 值为 value 的 Entry。
需要注意的是 Entry 中的 key 是弱引用, 当 ThreadLocal 外部强引用被置为 null (ThreadLocalInstance = null) 时, 那么系统 GC 的时候,
根据可达性分析, 这个 ThreadLocal 实例就没有任何一条链路能够引用到它, 这个 ThreadLocal 势必会被回收。 这样一来, ThreadLocalMap 中就会出现
key 为 null 的 Entry, 就没有办法访问这些 key 为 null 的 Entry 的 value。如果当前线程再迟迟不结束的话, 这些 key 为 null 的 Entry 的
value 就会一直存在一条强引用链: Thread Ref -> Thread -> ThreadLocalMap -> Entry -> value 永远无法回收, 造成内存泄漏。
当然, 如果当前 Thread 运行结束, ThreadLocal, ThreadLocalMap, Entry 没有引用链可达, 在垃圾回收的时候都会被系统进行回收。
在实际开发中, 会使用线程池去维护线程的创建和复用, 比如固定大小的线程池, 线程为了复用是不会主动结束的。所以, ThreadLocal 的内存泄漏问题, 是应该值得我们思考和注意的问题。
创建方式一
static class ThreadLocalMap {
// 存放数据的数组
private Entry[] table;
// 数组当前的元素个数
private int size = 0;
// 数组中的元素个数达到了这个就会扩容, 而不是数组满了才扩容
private int threshold;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// INITIAL_CAPACITY = 16;
table = new Entry[INITIAL_CAPACITY];
// 调用 ThreadLocal 的 threadLocalHashCode 的 threadLocalHashCode 属性值, 然后 & (16 - 1)
// 得到这个 firstKey 应该存在数组的哪个位置
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 在数组对应的位置存入 Entry
table[i] = new Entry(firstKey, firstValue);
// 当前容量设置为 1
size = 1;
// setTheshold 里面只有一行代码 threshold = len * 2 / 3;
// 也就是设置阈值 = 长度 * 2/3
setThreshold(INITIAL_CAPACITY);
}
}
创建方式二
static class ThreadLocalMap {
private ThreadLocalMap(ThreadLocalMap parentMap) {
// 取到入参 ThreadLocalMap 的数组
Entry[] parentTable = parentMap.table;
// 入参 ThreadLocalMap 的数组的长度, 这里的长度不用做任何处理, 因为这里的长度必定为 2 的 n 次方
int len = parentTable.length;
// 设置阈值 = len * 2/3
setThreshold(len);
// 设置新的数组
table = new Entry[len];
// 遍历入参的 ThreadLocalMap 的数组
for (int j = 0; j < len; j++) {
// 获取到 table 数组的第 j 个位置
Entry e = parentTable[j];
if (e != null) {
// 对应的位置有数据
// 从 e 中获取对应的 key 即 ThreadLocal 实例
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
// 调用 ThreadLocal 的 childValue 方法
// 在 ThreadLocal 中这个方法的默认实现为 throw new UnsupportedOperationException(); 抛出一个异常
// 也就是这里的 key 必须是 TheadLocal 的子类, 同时重写了 childValue 方法, 否则会报错
Object value = key.childValue(e.value);
// 把 key 和 value 封装为 Entry
Entry c = new Entry(key, value);
// 定位到放在数组的位置
int h = key.threadLocalHashCode & (len - 1);
// 对应的位置不为空, 查询后面的一个位置, 到了尾部, 回到头部继续往后找
while (table[h] != null)
h = nextIndex(h, len);
// 对应的位置 设置为当前的 Entry
table[h] = c;
// 个数加 1
size++;
}
}
}
}
/**
* 位置的定位
*/
private static int nextIndex(int i, int len) {
// 当前的位置 + 1 小于当前的长度, 取 + 1 的位置, 否则取 0, 既头部的位置
return ((i + 1 < len) ? i + 1 : 0);
}
}
从上面的方法中可以看出一个有趣的点:
- HashMap 在处理 hash 碰撞时使用的是: 拉链法
- ThreadLocalMap 在处理 hash 碰撞时使用的是: 开放定址法 (一旦产生了冲突, 就按某种规则去寻找另一空地址) 中的线性探测法。
具体的开放地址法如何解决 hash 冲突的可以看一下这篇文章: 冲突处理方法----开放定址法
在 ThreadLocalMap 中 key 对应的 Entry 在 Entry[] table 的位置的计算方式为 key.threadLocalHashCode & (len - 1), 这里的 key 就是我们传入的 ThreadLocal。
决定位置的元素有 2 个
- 当前 Entry[] table 数组的长度
- key 对应的 ThreadLocal 中的 threadLocalHashCode 的值 (这个 code 就是上面我们一直说的 ThreadLocal 内部的 code)
Entry[] table 的长度很容易知道, 但是 threadLocalHashCode 是如何计算的呢
public class ThreadLocal<T> {
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
// 这个值为 1640531527
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
}
先说结论:
每个 ThreadLocal 实例的 threadLocalHashCode 在 ThreadLocal 实例创建出来后就不会再变了, 这个 ThreadLocal 实例后面在使用中, threadLocalHashCode 固定的。
每个 ThreadLocal 实例的 threadLocalHashCode 都是不同的。
- nextHashCode 这个变量在 ThreadLocal 中被 static 修饰了, 说明所有的 ThreadLocal 的实例共用 1 个 AtomicInteger 的属性
- 当我们声明第 1 个 ThreadLocal, 调用唯一的 nextHashCode, 得到了当前的 threadLocalHashCode 值, 值为 0, 同时让唯一的 AtomicInteger 的值增加 1640531527
- 声明第 2 个 ThreadLocal, 又调用了唯一的 nextHashCode, 得到了当前的 threadLocalHashCode 值, 值为 1640531527, 然后又让 AtomicInteger 的值增加 1640531527
- 这样每创建一个 ThreadLocal 的 nextHashCode 都是不一样的。不要忽略了 int 值的取值范围, 导致值的变化
- 为什么每个 ThreadLocal 之间的的 nextHashCode 的差值为 1640531527 ? 这个和斐波那契散列有关, 它可以使 hashcode 均匀的分布在大小 2 的 N 次方的数组里。至于为什么, 涉及到数学相关的知识, pass !
通过上面的分析基本知道了 ThreadLocal 的大体实现了吧
- 每次声明了一个 ThreadLocal 实例, 这时候这个实例会得到一个固定的 hashCode, 存储在 threadLocalHashCode 变量中
- 每个线程中都有一个变量 threadLocals, 类型为 ThreadLocalMap
- ThreadLocalMap 本质是一个 Entry 的数组, Entry 对象中存储了 key, value, key 默认一个 ThreadLocal 的实例内部的 hash code, value 就是通过 ThreadLocal 存储到 Thread 的内容
- 当我们调用 ThreadLocal 的实例, 将内容存储到当前的 Thread 本质是存在 Thread 中的 threadLocals 属性, 也就是 Entry[] 数组中
调用 ThreadLocal 的实例, 存放数据的步骤大体如下
- 创建一个 ThreadLocal 的实例, 实例的得到一个接近唯一的 hashCode, 存储在自身的 threadLocalHashCode 变量
- 获取当前的线程维护的 threadLocals 变量, 数据类型为 ThreadLocalMap, 本质就是一个 Entry[] 的数组, 如果为空, 进行初始化
- 将当前的 ThreadLocal 实例内部的 threadLocalHashCode 作为 key, 需要存储的内容做为 value, 封装为一个 Entry 实例
- 通过当前的 ThreadLocal 实例的 threadLocalHashCode 和当前 Entry[] 数组的长度计算出, 这个 ThreadLocal 存储在数组中的哪一个位置, 把封装好的 Entry 实例放到数组中
- 这时候又声明了一个新的 ThreadLocal 的实例, 又是另一个 threadLocalHashCode
- 往这个新的 ThreadLocal 的实例存值, 同样是封装为 Entry,
- 当前线程的 threadLocals 变量不为空, 不会初始化, 也就是新的 ThreadLocal 和旧的 ThreadLocal 用着同一个 threadLocals 变量, 也就是同一个 Entry[] 数组
- 但是新的 ThreadLocal 的实例的 threadLocalHashCode 不一样, 计算出来的位置也会不一样, 这样放到同一个 Entry[] 数组的不同位置
- 也就是说使用 ThreadLocal 存储数据时, 数据是存储在当前线程的数组中的
static class ThreadLocalMap {
private void set(ThreadLocal<?> key, Object value) {
// 获取到当前存数据的数组引用
Entry[] tab = table;
// 数组长度
int len = tab.length;
// 当前 ThreadLocal 的 threadLocalHasCode & (数组的长度 - 1), 得到当前应该存在数组的哪个位置
int i = key.threadLocalHashCode & (len-1);
// 取到对应位置的元素, 但 i 位置的 不为 null
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
// 这时候的 e 一定不会是 null
// 从 Entry e 中获取 key
ThreadLocal<?> k = e.get();
// 对应的 k == 输入的 key, 替换新值, 结束
if (k == key) {
e.value = value;
return;
}
// 对应的位置的 k 为 null, 将当前的 value 设置到 ThreadLocalMap 的数组
// Entry 不为 null, 但是 Entry 的 key 为 null, 情况是存在的, 因为 Entry 中的 key 是一个弱引用, 能被 GC 直接回收
// 这时候可以把当前的 Entry 放到这个位置
if (k == null) {
// 将当前的 value 放到数组 i 的位置, 进行 table 数组的重新整理, 即对数组中, 无效的 Entity (key 为 null) 进行移除
// 然后对数组中的 Entry 通过其 key 的 hashCode 计算出来的位置和这个 Entry 当前所在的位置不一致 (hash 冲突过), 进行重试计算位置
// 这样经过了无效 Entry 删除后, 重新计算位置, 可能将其重新放到正确的位置
replaceStaleEntry(key, value, i);
return;
}
// 到了这里, 会执行 for 后面的 e = tab[i = nextIndex(i, len)], e != null, 又有进入到这个循环
// 等于 null 的话, 跳出循环, 执行下面的逻辑
}
// i 位置为 null, 创建一个新的 Entry 放到数组的 i 位置
tab[i] = new Entry(key, value);
// 容量加 1
int sz = ++size;
// cleanSomeSlots 没有清除过无效的 Entry, 同时当前数组中的数据已经大于阈值了, 进行扩容
// 插入后再次清除一些 key 为 null 的 "脏" entry, 如果大于阈值还是需要扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
// 扩容
rehash();
}
}
static class ThreadLocalMap {
/**
* 移除数组中的部分无效 Entry, 然后对范围内的 Entry 重新地位
*
* @param key ThreadLocal 实例 的 key
* @param value 需要存储的值
* @param staleSlot key 当前打算放到这个位置, 同时这个位置的 Entry 的 key 为 null, 为当前 key 的最优位置
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// 下面的所有的遍历, 基本都是
// 1. 向前遍历, 遍历到数组的头部后, 会继续从数组的尾部开始继续往前遍历
// 2. 向后遍历, 遍历到数组的尾部后, 会继续从数组的头部开始继续向后遍历
// 所以把存储数据的 table 数组看成一个头尾相接的环, 可以容易理解
// staleSlot 位置的 Entry key 为 null
// 在向前遍历中, 遇到的 Entry key 问 null, slotToExpunge 就会变更为这个 Entry 所在的数组位置
int slotToExpunge = staleSlot;
// 从当前的 staleSlot 位置向前(如果到了头部, 则从尾部继续往前找), 直到遇到第一个位置 Entry 为空才停止
// prevIndex 的逻辑就一句话: ((i - 1 >= 0) ? i - 1 : len - 1);
// 位置 i - 1 >=0, 没到数组的头部, 向前一位,
// 位置 i - 1 <0, i 已经是数组的头部了, 那么从 len - 1, len 为数组长度, len - 1 就是数组的尾部
// 往前找的过程中, slotToExpunge 会不断重置, 当前位置的 Entry 的 key 为 null, slotToExpunge 更新为这个 Entry 所在的位置
// 这个循环直到 (e = tab[i]) == null 才结束, 就是对应的位置为空, 没有数据
for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
// 对应对象的 key 为 null, 变更 slotToExpunge 等于当前的数组索引位置 i
if (e.get() == null)
slotToExpunge = i;
// 从 staleSlot 开始向后找(如果到了尾部了, 从头部继续找), 直到遇到第一个位置没有数据的才结束
// 从当前的 staleSlot 位置向后找(如果到了尾部了, 则从头部继续找后找), 直到下面 2 种情况才停止
// 1. 直到遇到第一个位置 Entry 为空才停止, 把当前的 Entry 放到 staleSlot 的位置
// 2. 在遍历中找到了 Entry 的 key 和当前要存储的 Entry 的 key 一样 => 将当前的 value 放到 找到的 Entry, 同时找到的 Entry 和 staleSlot 的 Entry 交换
// nextIndex 的逻辑就一句话: ((i + 1 < len) ? i + 1 : 0);
// 位置 i + 1 < len, 没到数组的尾部部, 向后一位,
// 位置 i + 1 >= len, i 已经是数组的尾部了, 从数组的第 0 位, 也就是头部开始
// 这个循环直到 (e = tab[i]) == null 才结束, 就是对应的位置为空, 没有数据
for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
// 在这里面的 e 不为 null
// 获取 entity 的 key, 也就是 ThreadLocal
ThreadLocal<?> k = e.get();
// 当前的 Entry 的 key 等于我们要存储的数据的 key
if (k == key) {
// 更新当前的 Entity 的 value 为我们要存储的 value
e.value = value;
// 上面的入参说过了, staleSlot 是当前需要存储的 ThreadLocal key 计算后存储的最优位置, 同时这个位置的 Entry 的 key 为 null
// 所以将 staleSolt 和当前找到的 key 一样的 Entry 进行交换
// 将 i 和一开始无效的位置 staleSlot 替换
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 如果上面的向前查找过程中没有更新 slotToExpunge 的值, 则更新 slotToExpunge 为当前值 i, staleSlot 的值是存在的
// slotToExpunge 初始值 = staleSlot, 在上面的向前找 Entry 中, 如果 staleSlot 向前的到第一个为空的距离间, 所有的 Entry 的 key 都不为 null
// slotTOExpunge 没有更新, 也就是还是等于 staleSlot
if (slotToExpunge == staleSlot)
// 从 nextIndex, 可以得知 i 是在 staleSlot 的后面的, 不等于 staleSlot
// 那么把 i 赋值诶 slotToExpunge, 此时的 i 是 Entry key 为空的元素
slotToExpunge = i;
// expungeStaleEntry 将从 slotToExpunge 到向后遇到的第一个为空的位置(环形的遍历, 可以从尾部回到头部), 这段距离
// 1. 无效的 Entry, 既 Entry 的 key 为 null 的清空
// 2. 有效的 Entry, 拿到 key 的 hashCode 重新计算自身的最优位置, 最优位置和当前 Entry 所在的位置不一样, 重试重新调整这个 Entry 的位置
// 同时返回遇到的第一个为空的位置的位置索引
// cleanSomeSlots 从第一个入参的位置开始, 向后遍历 log2(第二个入参) 次, 同样是环形遍历,
// 同样是 对 Entry 的 key 为 null 的进行清除, 对有效 Entity 的位置不等于直接计算出来的, 重新分配位置
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// key 为 null 并且当前的 slotToExpunge 还是等于 staleSlot(也就是上面向前找的时候, 没有找到符合条件的),
// 更新 slotToExpunge 为当前的 i(这时候的 entity 为无效 entity)
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
}
// 把 staleSlot 位置设置为当前的 key 和 value 组合的 Entity
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 如果 slotToExpunge 和 staleSlot 不行等, 表示有数据可以清除, 调用 expungeStaleEntry 和 cleanSomeSlots 进行清除
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
/*
* 从入参的位置开始, 向后遍历到第一个位置为空的位置 (环形遍历),
* 将这段距离中的无效 Entity 移除, 有效 Entity 的位置不等于直接计算出来的, 重新分配位置
* @param staleSlot 开始移除的位置
* @return 遇到的第一个为 null 的数组位置
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 先直接将 staleSlot 位置的置为 null
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
// nextIndex 和 后面的 (e = tail[i]) != null, 老套路
// 从 staleSolt 向后遍历, 直到遇到第一个为 null 停止, 注意是环形的, 会从尾部回到头部
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
// 当前位置的 Entry 的 key 为 null, 清掉
e.value = null;
tab[i] = null;
size--;
} else {
// 当前位置的 Entry 的 key 不为 null
// 获取这个 Entry 的 key 直接计算后的直接存储位置
int h = k.threadLocalHashCode & (len - 1);
// 当前存储的位置和直接计算出来的位置不一样, 重新试着调整一下位置
if (h != i) {
// 把当前的位置置为 null, 方便下面的循环, 最坏的情况能到这个位置停下来, 即重新计算后的位置和当前的一样, 是最优的位置了
tab[i] = null;
// 从计算出来的位置 h 向后遍历到第一个为 null 的位置 (nextIndex 环形的)
while (tab[h] != null)
h = nextIndex(h, len);
// 把计算出来的新位置设置为当前的 Entry
tab[h] = e;
}
}
}
return i;
}
/**
* 从 i 位置开始, 向后遍历 log2(n) 次, 同样是环形遍历,
* 在这 log2(n) 次遍历中, 对 Entry 的 key 为 null 的进行清除, 对有效 Entity 的位置不等于直接计算出来的, 重新分配位置
* 对无效的 entity 进移除和对算出来的位置不一致的重新计算
*
* 控制遍历的次数在 log2(n) 次数, 是一个时间上的折中控制, 全数组扫描会和耗时, 扫描次数太少又没太大意义
*
* @param i 数组中为空的位置
* @param n 需要扫描的次数, 一般都是当前数组的长度
*/
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
// i 的下一个位置 (环形的)
i = nextIndex(i, len);
Entry e = tab[i];
// 下一个位置不为空, 但是 key 为空
if (e != null && e.get() == null) {
// n 更新为当前数组的禅道
n = len;
// 有数据被清空
removed = true;
// 调用到上面的 expungeStaleEntry 方法
// 对从 i 向后遍历遇到的一个位置为空的距离(环形遍历), 清空无效的 Entry, 将有效 Entry 直接计算出来的位置和当前位置不一样的, 进行位置的重新调整
// 返回遇到的第一个位置为空的位置索引
i = expungeStaleEntry(i);
}
// n>>> 1 等于 n / 2
} while ( (n >>>= 1) != 0);
// 返回这次清洗中是否有无效 Entry 被清除过
return removed;
}
}
static class ThreadLocalMap {
private void rehash() {
// 对这个数组中的无效 Entry 调用 expungeStaleEntry(无效 Entry 的位置) 方法进行清理
expungeStaleEntries();
// 通过 expungeStaleEntries 尝试清理后
// 当前的数组容量还是大于 阈值的 3/4, 真正的扩容
if (size >= threshold - threshold / 4)
resize();
}
/**
* 对整个数组中的无效 Entry 所在的位置调用 expungeStaleEntry() 进行无效 Entry 的清除和有效 Entry 的重新分配位置
*/
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
// 遍历整个数组, 通过 expungeStaleEntry 进行数组数据的清除和位置重置
for (int j = 0; j < len; j++) {
Entry e = tab[j];
// 对数组中 Entry 不为 null, 但是 key 为 null 的位置
// 调用 expungeStaleEntry() 方法对当前数组从这个位置开始, 向后遍历到第一个为位置为空的位置 (环形遍历), 对这段距离内
// 将这段距离中的无效 Entity 移除, 有效 Entity 的位置不等于直接计算出来的, 重新分配位置
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
// 长度扩大为原来的 2 倍
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
// 遍历旧数组
for (Entry e : oldTab) {
// 不为空
if (e != null) {
ThreadLocal<?> k = e.get();
// 无效的 entity
if (k == null) {
// 值也置为空, 帮助 GC
e.value = null;
} else {
// 计算出新的位置
int h = k.threadLocalHashCode & (newLen - 1);
// 冲突检测, 如果对应的位置有值了, 则找下一个位置
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
// 个数加 1
count++;
}
}
}
// 更新阈值为当前长度的 2/3
setThreshold(newLen);
size = count;
table = newTab;
}
}
上面就是这个 ThreadLocalMap 的 set 方法的整体流程, 繁杂的一个大流程, 整理后, 我们可以知道
1. 怎样确定新值插入到哈希表中的位置
每个 ThreadLocal 的 threadLocalHashCode 都是在 ThreadLocal 声明的时候就确定了, 通过 ThreadLocal 实例的 threadLocalHashCode & (当前数组的长度 - 1) 就能得到当前的 value 放在数组的位置
2. 当存放的位置出现冲突了, 怎么解决冲突的
源码中通过 nextIndex(i, len) 方法解决 hash 冲突的问题, 该方法为 ((i + 1 < len) ? i + 1 : 0);
也就是不断往后线性探测, 当到哈希表末尾的时候再从 0 开始, 成环形, 直到找到数据为空的位置, 这个就是这个 Entry 存放的地方了
3. 怎样解决无效 Entry
在 set() 方法的 for 循环中寻找和当前 Key 相同的可覆盖 Entry 的过程中通过 replaceStaleEntry 方法解决脏 entry 的问题。
如果当前 table[i] 为 null 的话, 直接插入新 Entry 后也会通过 cleanSomeSlots() 来解决脏 entry 的问题
4. 扩容的时机和方式
扩容的时机:
表面上是当前存放数据的个数 = 数组的长度 * 3/4;
而实际是 当前存放数据的个数 = 数组的长度 * 3/4 时, 会触发一次全数组的无效 Entry 清除和有效 Entry 的位置重算。
经过清除和重算后, 当前存放数据的个数还是 >= 当前阈值的 *3/4 (阈值 = 数组的长度 * 2/3) = 当前数组的 1/2 就扩容了
扩容的方式: 当前的长度变为 2 倍, 然后把旧的数据移到新的数组中
static class ThreadLocalMap {
private Entry getEntry(ThreadLocal<?> key) {
// 计算出位置
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// e 不为空, 同时对应的 key 一致, 也就是直接找到了, 就返回这个 entity
if (e != null && e.get() == key)
return e;
else
// 找到的位置的 Entry 为空 或者 Entry 的 key 不等于当前的 key
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 变量 Entry 不为空, 也就是当前的 key 直接计算出来的位置有数据, 才能进入到下面的循环, 否则直接返回 null
while (e != null) {
ThreadLocal<?> k = e.get();
// 再次判断获取到的 Entry 的 key 是否等于当前的 key
if (k == key)
// 一样直接返回入参的 Entry
return e;
// k 为 null
if (k == null)
// 无效 Entry 回收, 有效 Entry 位置重置
expungeStaleEntry(i);
else
// 下一个位置
i = nextIndex(i, len);
// 获取新位置的 Entry
e = tab[i];
}
// 返回 null
return null;
}
}
static class ThreadLocalMap {
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 计算出位置
int i = key.threadLocalHashCode & (len-1);
// 遍历 i 和其后面的 Entity, 直到 key 找到了或者遇到了空
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
// 无效 Entry 清除, 有效 Entry 位置重算
expungeStaleEntry(i);
return;
}
}
}
}
ThreadLocal 不是用来解决共享对象的多线程访问问题的, 数据实质上是放在每个 Thread 实例引用的 ThreadLocalMap, 也就是说每个不同的线程都拥有专属于自己的数据容器 (ThreadLocalMap), 彼此不影响。
因此 ThreadLocal 只适用于 共享对象会造成线程安全 的业务场景。
比如 Hibernate 中通过 HhreadLocal 管理 Session 就是一个典型的案例, 不同的请求线程 (用户) 拥有自己的 session, 若将 session 共享出去被多线程访问, 必然会带来线程安全问题。
在 Java 中, 我们声明了一个对象, 这个对象一般情况下是在堆中, 而创建的线程只是持有对象的引用, 通过这个引用调用创建的对象。
而对于这个对象的内存分配和内存回收, 都不需要程序员负责, 都是由 JVM 去负责。一个对象是否可以被回收, 主要看是否有引用指向此对象, 说的专业点, 叫可达性分析。
所以,引用对应 JVM 有着重要的作用。
Java 设计这四种引用的主要目的有两个
- 可以让程序员通过代码的方式来决定某个对象的生命周期
- 有利用垃圾回收
1. 强引用
只要某个对象有强引用与之关联, 这个对象永远不会被回收, 即使内存不足, JVM 宁愿抛出 OOM, 也不会去回收
2. 软引用
当内存不足, 会触发 JVM 的 GC, 如果 GC 后, 内存还是不足, 就会把软引用的包裹的对象给干掉, 也就是只有在内存不足, JVM 才会回收该对象
3. 弱引用
不管内存是否足够, 只要发生 GC, 都会被回收
4. 虚引用
- 无法通过虚引用来获取对一个对象的真实引用
- 虚引用必须与 ReferenceQueue 一起使用, 当 GC 准备回收一个对象, 如果发现它还有虚引用, 就会在回收之前, 把这个虚引用加入到与之关联的 ReferenceQueue 中
- 当发生 GC, 虚引用就会被回收, 并且会把回收的通知放到 ReferenceQueue 中
并发容器之ThreadLocal
ThreadLocal源码深入剖析
强软弱虚引用, 只有体会过了, 才能记住