ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。
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; // 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置 int i = key.threadLocalHashCode & (len - 1); // 使用线性探测法查找元素 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal <?> k = e.get(); // ThreadLocal 对应的 key 存在,直接覆盖之前的值 if (k == key) { e.value = value; return; } // key为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,当前数组中的 Entry 是一个陈旧(stale)的元素 if (k == null) { // 用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏,具体可以看源代码,没看太懂 replaceStaleEntry(key, value, i); return; } } // ThreadLocal 对应的 key 不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的 Entry。 tab[i] = new Entry(key, value); int sz = ++size; // cleanSomeSlot 清理陈旧的 Entry(key == null),具体的参考源码。如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值,则进行 rehash。 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
set 方法大致流程如下:
1、获取当前线程的成员变量map 2、map非空,则重新将ThreadLocal和新的value副本放入到map中 3、map空,则对线程的成员变量ThreadLocalMap进行初始化创建,并将ThreadLocal和value副本放入map中。
注意:
1、int i = key.threadLocalHashCode & (len - 1);,这里实际上是对 len-1 进行了取余操作。之所以能这样取余是因为 len 的值比较特殊,是 2 的 n 次方,减 1 之后低位变为全 1,高位变为全 0。例如 16,减 1 之后对应的二进制为: 00001111,这样其他数字中大于 16 的部分就会被 0 与掉,小于 16 的部分就会保留下来,就相当于取余了。
2、在 replaceStaleEntry 和 cleanSomeSlots 方法中都会清理一些陈旧的 Entry,防止内存泄漏(关于内存泄漏,下面会讲)。
3、threshold 的值大小为threshold = len * 2 / 3;
4、rehash 方法中首先会清理陈旧的 Entry,如果清理完之后元素数量仍然大于 threshold 的 3/4,则进行扩容操作(数组大小变为原来的 2倍)。
public T get() { //1. 获取当前线程的实例对象 Thread t = Thread.currentThread(); //2. 获取当前线程的threadLocalMap ThreadLocalMap map = getMap(t); if (map != null) { //3. 获取map中当前threadLocal实例为key的值的entry ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") //4. 当前entitiy不为null的话,就返回相应的值value T result = (T)e.value; return result; } } //5. 若map为null或者entry为null的话通过该方法初始化,并返回该方法返回的value return setInitialValue(); }
大致流程如下:
1、获取当前线程的ThreadLocalMap对象threadLocals 2、从map中获取线程存储的K-V Entry节点。 3、从Entry节点获取存储的Value副本值返回。 4、map为空的话返回初始值null,即线程变量副本为null,在使用时需要注意判断NullPointerException。
ThreadLocal每个线程维护一个 ThreadLocalMap 的映射表,映射表的 key 是 ThreadLocal 实例本身,value 是要存储的副本变量。ThreadLocal 实例本身并不存储值,它只是提供一个在当前线程中找到副本值的 key。如下图所示:
我们从下面三个方面看下 ThreadLocal 的实现:
线程使用 ThreadLocalMap 来存储每个线程副本变量,它是 ThreadLocal 里的一个静态内部类。ThreadLocalMap 也是采用的散列表(Hash)思想来实现的,但是实现方式和 HashMap 不太一样。
我们首先看下散列表的相关知识:
理想状态下,散列表就是一个包含关键字的固定大小的数组,通过使用散列函数,将关键字映射到数组的不同位置。下面是理想散列表的一个示意图:
在理想状态下,哈希函数可以将关键字均匀的分散到数组的不同位置,不会出现两个关键字散列值相同(假设关键字数量小于数组的大小)的情况。但是在实际使用中,经常会出现多个关键字散列值相同的情况(被映射到数组的同一个位置),我们将这种情况称为散列冲突。为了解决散列冲突,主要采用下面两种方式:
分离链表法
分散链表法使用链表解决冲突,将散列值相同的元素都保存到一个链表中。当查询的时候,首先找到元素所在的链表,然后遍历链表查找对应的元素。下面是一个示意图:
开放定址法
开放定址法不会创建链表,当关键字散列到的数组单元已经被另外一个关键字占用的时候,就会尝试在数组中寻找其他的单元,直到找到一个空的单元。探测数组空单元的方式有很多,这里介绍一种最简单的 -- 线性探测法。线性探测法就是从冲突的数组单元开始,依次往后搜索空单元,如果到数组尾部,再从头开始搜索(环形查找)。如下图所示:
ThreadLocalMap 中使用开放地址法来处理散列冲突,而 HashMap 中使用的分离链表法。之所以采用不同的方式主要是因为:在 ThreadLocalMap 中的散列值分散的十分均匀,很少会出现冲突,并且 ThreadLocalMap 经常需要清除无用的对象,使用纯数组更加方便。
解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。主要逻辑如下:
/** * Increment i modulo len. */ private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } /** * Decrement i modulo len. */ private static int prevIndex(int i, int len) { return ((i - 1 >= 0) ? i - 1 : len - 1); }
每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。
我们知道 Map 是一种 key-value 形式的数据结构,所以在散列数组中存储的元素也是 key-value 的形式。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 将 ThreadLocal 实例作为 key,副本变量作为 value 存储起来。
注意 Entry 中对于 ThreadLocal 实例的引用是一个弱引用(这里为啥用弱引用,稍后会解析。),该引用定义在 Reference 类(WeakReference的父类)中,下面是 super(k) 最终调用的代码:
Reference(T referent) { this(referent, null); } Reference(T referent, ReferenceQueue <? super T> queue) { this.referent = referent; this.queue = (queue == null) ? ReferenceQueue.NULL : queue; }
存取的基本流程就是首先获得当前线程的 ThreadLocalMap,将 ThreadLocal 实例作为键值传入 Map,然后就是进行相关的变量存取工作了。线程中的 ThreadLocalMap 是懒加载的,只有真正的要存变量时才会调用 createMap 创建
当创建了一个 ThreadLocal 的实例后,它的散列值就已经确定了,下面是 ThreadLocal 中的实现:
/** * ThreadLocals rely on per-thread linear-probe hash maps attached * to each thread (Thread.threadLocals and * inheritableThreadLocals). The ThreadLocal objects act as keys, * searched via threadLocalHashCode. This is a custom hash code * (useful only within ThreadLocalMaps) that eliminates collisions * in the common case where consecutively constructed ThreadLocals * are used by the same threads, while remaining well-behaved in * less common cases. */ 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
是一个常量,它通过 nextHashCode()
函数产生。nextHashCode()
函数其实就是在一个 AtomicInteger 变量(初始值为0)的基础上每次累加 0x61c88647
,使用 AtomicInteger 为了保证每次的加法是原子操作。而 0x61c88647
这个就比较神奇了,它可以使 hashcode 均匀的分布在大小为 2 的 N 次方的数组里。其实 0x61c88647
就是 FibonacciHashing
由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。
ThreadLocal<Session> threadLocal = new ThreadLocal<Session>(); try { threadLocal.set(new Session(1, "Misout的博客")); // 其它业务逻辑 } finally { threadLocal.remove(); }
还记得Hibernate的session获取场景吗?
private static final ThreadLocal<Session> threadLocal = new ThreadLocal<Session>(); //获取Session public static Session getCurrentSession(){ Session session = threadLocal.get(); //判断Session是否为空,如果为空,将创建一个session,并设置到本地线程变量中 try { if(session ==null&&!session.isOpen()){ if(sessionFactory==null){ rbuildSessionFactory();// 创建Hibernate的SessionFactory }else{ session = sessionFactory.openSession(); } } threadLocal.set(session); } catch (Exception e) { // TODO: handle exception } return session; }
为什么?每个线程访问数据库都应当是一个独立的Session会话,如果多个线程共享同一个Session会话,有可能其他线程关闭连接了,当前线程再执行提交时就会出现会话已关闭的异常,导致系统异常。此方式能避免线程争抢Session,提高并发下的安全性。
使用ThreadLocal的典型场景正如上面的数据库连接管理,线程会话管理等场景,只适用于独立变量副本的情况,如果变量为全局共享的,则不适用在高并发下使用。
1、每个ThreadLocal只能保存一个变量副本,如果想要上线一个线程能够保存多个副本以上,就需要创建多个ThreadLocal。
2、ThreadLocal内部的ThreadLocalMap键为弱引用,会有内存泄漏的风险。
3、适用于无状态,副本变量独立后不影响业务逻辑的高并发场景。如果如果业务逻辑强依赖于副本变量,则不适合用ThreadLocal解决,需要另寻解决方案。
https://www.cnblogs.com/zhangjk1993/archive/2017/03/29/6641745.html https://www.jianshu.com/p/98b68c97df9b
本文分享自微信公众号 - 爱编码(ilovecode),作者:zero
原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。
原始发表时间:2019-07-28
本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。
我来说两句