并发编程之ThreadLocal (2)

其中的 HASH_INCREMENT 也不是随便取的,它转化为十进制是 1640531527,2654435769 转换成 int 类型就是 -1640531527,2654435769 等于 (√5-1)/2 乘以 2 的 32 次方。(√5-1)/2 就是黄金分割数,近似为 0.618,也就是说 0x61c88647 理解为一个黄金分割数乘以 2 的 32 次方,它可以保证 nextHashCode 生成的哈希值,均匀的分布在 2 的幂次方上,且小于 2 的 32 次方。

下面用例子来证明下:

@Slf4j public class ThreadLocalUtil2 { private static final int HASH_INCREMENT = 0x61c88647; public static void main(String[] args) { int n = 5; int max = 2 << (n - 1); for (int i = 0; i < max; i++) { System.out.print(i * HASH_INCREMENT & (max - 1)); System.out.print(" "); } } }

运行结果为:

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

可以发现元素索引值完美的散列在数组当中,并没有出现冲突。

ThreadLocalMap

除了上述属性外,还有一个重要的属性 ThreadLocalMap,ThreadLocalMap 是 ThreadLocal 的静态内部类,当一个线程有多个 ThreadLocal 时,需要一个容器来管理多个 ThreadLocal,ThreadLocalMap 的作用就是管理线程中多个 ThreadLocal,源码如下:

static class ThreadLocalMap { // 键值对的存储结构 static class Entry extends WeakReference<ThreadLocal<?>> { // ThreadLocal 对应的value值 Object value; Entry(ThreadLocal<?> k, Object v) { // ThreadLocal 是弱引用的,当GC时会被回收掉,但是 value 不会被回收 super(k); value = v; } } // 默认初始容量 16 必须是2的幂 private static final int INITIAL_CAPACITY = 16; // 底层时 Entry 数组,根据需要进行扩容,数组的大小必须是 2 的幂 private Entry[] table; // 数组的大小 private int size = 0; // 数组的扩容阈值 默认是 0 private int threshold; // 数组扩容阈值为 长度的 2/3 private void setThreshold(int len) { threshold = len * 2 / 3; } }

从源码中看到 ThreadLocalMap 其实就是一个简单的 Map 结构,底层是数组,有初始化大小,也有扩容阈值大小,数组的元素是 EntryEntry 的 key 就是 ThreadLocal 的引用,value 是 ThreadLocal 的值。ThreadLocalMap 解决 hash 冲突的方式采用的是线性探测法,如果发生冲突会继续寻找下一个空的位置。

这样的就有可能会发生内存泄漏的问题,下面让我们进行分析:

ThreadLocal 内存泄漏

ThreadLocal 在没有外部强引用时,发生 GC 时会被回收,那么 ThreadLocalMap 中保存的 key 值就变成了 null,而 Entry 又被 threadLocalMap 对象引用,threadLocalMap 对象又被 Thread 对象所引用,那么当 Thread 一直不终结的话,value 对象就会一直存在于内存中,也就导致了内存泄漏,直至 Thread 被销毁后,才会被回收。

那么如何避免内存泄漏呢?

在使用完 ThreadLocal 变量后,需要我们手动 remove 掉,防止 ThreadLocalMap 中 Entry 一直保持对 value 的强引用,导致 value 不能被回收,其中 remove 源码如下所示:

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

remove 方法的时序图如下所示:

并发编程之ThreadLocal

remove 方法是先获取到当前线程的 ThreadLocalMap,并且调用了它的 remove 方法,从 map 中清理当前 ThreadLocal 对象关联的键值对,这样 value 就可以被 GC 回收了。

那么 ThreadLocal 是如何实现线程隔离的呢?

ThreadLocal 的 set 方法

我们先去看下 ThreadLocal 的 set 方法,源码如下:

// set 方法 public void set(T value) { // 获取当前thread信息 Thread t = Thread.currentThread(); // 获取当前线程所在的 ThreadLocalMap ThreadLocalMap map = getMap(t); // map 不为空直接set值 if (map != null) map.set(this, value); else // map 为空时,需要先创建 map createMap(t, value); } // 创建map,并保存值 void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }

set 方法的作用是把我们想要存储的 value 给保存进去。set 方法的流程主要是:

先获取到当前线程的引用

利用这个引用来获取到 ThreadLocalMap

如果 map 为空,则去创建一个 ThreadLocalMap

如果 map 不为空,就利用 ThreadLocalMap 的 set 方法将 value 添加到 map 中

set 方法的时序图如下所示:

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wssxdy.html