ThreadLocal 是一个本地线程副本变量工具类.
主要用于将私有线程和该线程存放的副本对象做一个映射, 各个线程之间的变量互不干扰, 在高并发场景下, 可以实现无状态的调用, 特别适用于各个线程依赖不同的变量值完成操作的场景.
一, ThreadLocal 的核心机制
每个 Thread 线程内部都有一个 Map,Tread 类的 ThreadLocal.ThreadLocalMap 属性
Map 里面存储线程本地对象 (key 也就是当前的 ThreadLoacal 对象) 和线程的变量副本(value)
Thread 内部的 Map 是由 ThreadLocal 维护的, 由 ThreadLocal 负责向 map 获取和设置线程的变量值
数据结构:
二, ThreadLocal 源码分析
ThreadLocal 核心方法:
get(): 返回此线程局部变量的当前线程副本中的值.
initialValue(): 返回此线程局部变量的当前线程的 "初始值".
remove(): 移除此线程局部变量当前线程的值.
set(T value): 将此线程局部变量的当前线程副本中的值设置为指定值.
内部类 ThreadLocalMap:
- static class Entry extends WeakReference<ThreadLocal<?>> {
- /** The value associated with this ThreadLocal. */
- Object value;
- Entry(ThreadLocal<?> k, Object v) {
- super(k);
- value = v;
- }
- }
Entry 继承自 WeakReference(弱引用, 生命周期只能存活到下次 GC 前), 但只有 Key 是弱引用类型的, Value 并非弱引用.
ThreadLocalMap 的 set()方法:
- private void set(ThreadLocal<?> key, Object value) {
- ThreadLocal.ThreadLocalMap.Entry[] tab = table;
- int len = tab.length;
- // 根据 ThreadLocal 的散列值, 查找对应元素在数组中的位置
- int i = key.threadLocalHashCode & (len-1);
- // 采用 "线性探测法", 寻找合适位置
- for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
- e != null;
- e = tab[i = nextIndex(i, len)]) {
- ThreadLocal<?> k = e.get();
- // key 存在, 直接覆盖
- if (k == key) {
- e.value = value;
- return;
- }
- // key == null, 但是存在值(因为此处的 e != null), 说明之前的 ThreadLocal 对象已经被回收了
- if (k == null) {
- // 用新元素替换陈旧的元素
- replaceStaleEntry(key, value, i);
- return;
- }
- }
- // ThreadLocal 对应的 key 实例不存在也没有陈旧元素, new 一个
- tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
- int sz = ++size;
- // cleanSomeSlots 清楚陈旧的 Entry(key == null)
- // 如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值, 则进行 rehash
- if (!cleanSomeSlots(i, sz) && sz>= threshold)
- rehash();
- }
ThreadLocalMap 中解决 Hash 冲突的方式并非链表的方式, 而是采用线性探测的方式, 所谓线性探测, 就是根据初始 key 的 hashcode 值确定元素在 table 数组中的位置, 如果发现这个位置上已经有其他 key 值的元素被占用, 则利用固定的算法寻找一定步长的下个位置, 依次判断, 直至找到能够存放的位置.
ThreadLocalMap 解决 Hash 冲突的方式就是简单的步长加 1 或减 1, 寻找下一个相邻的位置.
显然 ThreadLocalMap 采用线性探测的方式解决 Hash 冲突的效率很低, 如果有大量不同的 ThreadLocal 对象放入 map 中时发送冲突, 或者发生二次冲突, 则效率很低.
所以这里引出的建议是: 每个线程只存一个变量, 这样的话所有的线程存放到 map 中的 Key 都是相同的 ThreadLocal, 如果一个线程要保存多个变量, 就需要创建多个 ThreadLocal, 多个 ThreadLocal 放入 Map 中时会极大的增加 Hash 冲突的可能.
get()方法:
步骤:
(1)获取当前线程的 ThreadLocalMap 对象 threadLocals
(2)从 map 中获取线程存储的 K-V Entry 节点.
(3)从 Entry 节点获取存储的 Value 副本值返回.
(4)map 为空的话返回初始值 null, 即线程变量副本为 null, 在使用时需要注意判断 NullPointerException.
- public T get() {
- // 获取当前线程
- Thread t = Thread.currentThread();
- // 获取当前线程的成员变量 threadLocal
- ThreadLocalMap map = getMap(t);
- if (map != null) {
- // 从当前线程的 ThreadLocalMap 获取相对应的 Entry
- ThreadLocalMap.Entry e = map.getEntry(this);
- if (e != null) {
- @SuppressWarnings("unchecked")
- // 获取目标值
- T result = (T)e.value;
- return result;
- }
- }
- return setInitialValue();
- }
- ThreadLocalMap getMap(Thread t) {
- return t.threadLocals;
- }
set()方法:
步骤:
(1)获取当前线程的成员变量 map
(2)map 非空, 则重新将 ThreadLocal 和新的 value 副本放入到 map 中.
(3)map 空, 则对线程的成员变量 ThreadLocalMap 进行初始化创建, 并将 ThreadLocal 和 value 副本放入 map 中.
- 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 getMap(Thread t) {
- return t.threadLocals;
- }
- void createMap(Thread t, T firstValue) {
- t.threadLocals = new ThreadLocalMap(this, firstValue);
- }
initialValue()方法:
该方法定义为 protected 级别且返回为 null, 很明显是要子类实现它的, 所以我们在使用 ThreadLocal 的时候一般都应该覆盖该方法. 该方法不能显示调用, 只有在第一次调用 get()或者 set()方法时才会被执行, 并且仅执行 1 次.
- protected T initialValue() {
- return null;
- }
三, 使用场景
简单使用示例 1:
- public class SeqCount {
- private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>(){
- // 实现 initialValue()
- public Integer initialValue() {
- return 0;
- }
- };
- public int nextSeq(){
- seqCount.set(seqCount.get() + 1);
- return seqCount.get();
- }
- public static void main(String[] args){
- SeqCount seqCount = new SeqCount();
- SeqThread thread1 = new SeqThread(seqCount);
- SeqThread thread2 = new SeqThread(seqCount);
- SeqThread thread3 = new SeqThread(seqCount);
- SeqThread thread4 = new SeqThread(seqCount);
- thread1.start();
- thread2.start();
- thread3.start();
- thread4.start();
- }
- private static class SeqThread extends Thread{
- private SeqCount seqCount;
- SeqThread(SeqCount seqCount){
- this.seqCount = seqCount;
- }
- public void run() {
- for(int i = 0 ; i <3 ; i++){
- System.out.println(Thread.currentThread().getName() + "seqCount :" + seqCount.nextSeq());
- }
- }
- }
- }
运行结果:
- Thread-1 seqCount :1
- Thread-3 seqCount :1
- Thread-3 seqCount :2
- Thread-3 seqCount :3
- Thread-0 seqCount :1
- Thread-0 seqCount :2
- Thread-0 seqCount :3
- Thread-2 seqCount :1
- Thread-1 seqCount :2
- Thread-1 seqCount :3
- Thread-2 seqCount :2
- Thread-2 seqCount :3
注意: initialValue()方法返回一个对象时, get()和 set()方法操作的其实是同一个对象的属性, 不能实现线程隔离.
使用场景二: session 获取场景
每个线程访问数据库都应当是一个独立的 Session 会话, 如果多个线程共享同一个 Session 会话, 有可能其他线程关闭连接了, 当前线程再执行提交时就会出现会话已关闭的异常, 导致系统异常. 此方式能避免线程争抢 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;
- }
四, 内存泄漏问题
ThreadLocalMap 使用 ThreadLocal 的弱引用作为 key, 如果一个 ThreadLocal 没有外部强引用来引用它, 那么系统 GC 的时候, 这个 ThreadLocal 势必会被回收, 这样一来, ThreadLocalMap 中就会出现 key 为 null 的 Entry, 就没有办法访问这些 key 为 null 的 Entry 的 value, 如果当前线程再迟迟不结束的话, 这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链: Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 永远无法回收, 造成内存泄漏.
其实, ThreadLocalMap 的设计中已经考虑到这种情况, 也加上了一些防护措施: 在 ThreadLocal 的 get(),set(),remove()的时候都会清除线程 ThreadLocalMap 里所有 key 为 null 的 value.
但是这些被动的预防措施并不能保证不会内存泄漏:
使用 static 的 ThreadLocal, 延长了 ThreadLocal 的生命周期, 可能导致的内存泄漏.
分配使用了 ThreadLocal 又不再调用 get(),set(),remove()方法, 那么就会导致内存泄漏.
内存泄漏实例分析: ThreadLocal 内存泄露的实例分析
解决:
每次使用完 ThreadLocal, 都调用它的 remove()方法, 清除数据.
在使用线程池的情况下, 没有及时清理 ThreadLocal, 不仅是内存泄漏的问题, 更严重的是可能导致业务逻辑出现问题. 所以, 使用 ThreadLocal 就跟加锁完要解锁一样, 用完就清理.
来源: https://www.cnblogs.com/hexinwei1/p/10032907.html