臭名昭著aTrust隔离计划1.臭名昭著aTrust隔离计划2.VPN原理与aTrust隔离网络实践3.docker-easyconnect到底做了什么4.TUN(tunnel-隧道-虚拟网卡)模式
个人项目
personal(个人展示)GitHub Actions 自动部署个人展示项目
基于Quartz搭建的个人博客1.Quartz个人博客使用教程2.使用 rsync 增量部署 Quartz 博客3.使用 GitHub Actions 自动部署 Quartz 博客5.域名绑定6.CDN加速7.Google 搜索收录排查:为什么关键词搜得到别人,却搜不到我的网站?Obsidian、双向链接与知识图谱
架构设计
分布式分布式事务RPC原理
旅行
行程攻略青岛三日游行程攻略
前端
NginxNginx配置与反向代理入门
Nodenpm与npx的区别
数据库
mysql01.MySQL基础语法与约束02.MySQL数据库类型03.MySQL多表查询04.MySQL 索引05.MySQL 事务06.MySQL 隔离级别07.MySQL MVCC08.MySQL 锁09.EXPLAIN10.慢查询与 SQL 优化
mysql面试题01.索引问题02.事务与隔离级别03.锁机制04.存储引擎与性能优化05.用户权限、字符集与备份恢复
mysql实战01.EXPLAIN实战02.测试环境菜单误删排查与恢复03.MySQL IN 详解
网络
网络安全1.无线监听网卡苹果M系列虚拟机安装Kali Linux教程3.kali怎么使用RTL8812AU4.kali基础命令
网络基础进程之间怎样通过网络传递数据
网址常用网址
中间键
esElasticsearch 入门
mq
RabbitMQRabbitMQ高级RabbitMQ基础
1. MQ 基础2. MQ 核心角色与代码对应3. MQ Consumer Group 与消费模式4. SOFAMQ 账户变更代码实践5. MQ 可靠性问题6. 消息幂等
redis缓存击穿、穿透与雪崩Redis 分布式锁Redis 缓存问题Redis 数据结构
AI
使用说明
第三方插件&技能简介Archify使用与安装指南geekgeekRun使用OfferFlow使用指南Ponytail使用指南Skills Manager使用指南
CodexCodex CLI与IDE区别及使用指南Codex Hook单独配置与提交通知Codex MCP安装与使用指南Codex第三方插件安装与使用指南
Agent开发0-学习路线1.LLM、Token、上下文窗口与模型参数2.大模型 API、请求参数与响应结构3.Spring AI ChatClient4.Prompt、System Prompt、Prompt 模板6.结构化输出、JSON Schema7.SSE 流式响应8.会话 ID、聊天记录、Redis9.超时、重试、限流、降级10.*完成可运行聊天接口11.Function Calling、Tool Calling 原理12.用 Java 方法定义 Tool13.工具参数设计和校验14.实现订单查询工具15.实现物流查询工具16.实现库存查询工具17.多个工具自动选择18.工具异常、超时、重试
diy主机
主机零件学习1.CPU2.主板3.内存4.显卡5.硬盘6.散热7.风扇8.机箱热门型号价格表
捡垃圾diy-13600kf+5060diy-14600kf+5070
English高频词汇英文基础语法简单句子音标
GitGit常用命令Sublime Merge 使用指南
Java
工具库Caffeine本地缓存Reactor教程
工作实践如何使用@Async线程池使用与排查线上服务 CPU 使用率过高排查压测下RPC接口大量失败排查记录
基础1.HashMap2.ConcurrentHashMap3.HashSet4.final5.static6.volatile7.AtomicInteger8.String动态代理反射锁与线程同步线程注解
面试题1.HashMap 存储在方法内是线程安全的吗2.ArrayList参数传递与引用重新赋值3.== 和 equals 的区别、hashcode4.重载和重写的区别5.String、StringBuffer、StringBuilder区别及使用场景6.接口和抽象类7.ArrayList 与 LinkedList 的区别和扩容机制8.HashMap 和 Hashtable 的区别及底层实现9.Object 和 Objects 的区别10.ThreadLocal的原理和使用场景11.字节码是什么及作用12.Java类加载器有哪些13.双亲委派模型14.Java异常体系15.GC是什么16.GC如何判断对象可以被回收
面试总结0.HR面试模板1.面试一2.面试二3.面试三笔试
网络与 IO(黑马程序员-纯理论)01-网络编程基础02-IO模型与零拷贝03-BIO04-NIO基础与缓冲区05-NIO通道与选择器06-AIO
juc00-目录01-进程与线程02-线程同步与通信03-并发设计模式04-Java内存模型05-CAS与原子类06-ThreadLocal07-阻塞队列08-线程池使用与原理09-任务调度10-ForkJoin与享元模式11-AQS12-ReentrantLock13-读写锁14-并发协作工具15-ConcurrentHashMap16-CopyOnWrite与同步容器17-SkipListMap18-非阻塞队列
juc(工作常见)Java并发编程三个重点拆解
jvm1. JVM运行时数据区
Spring1.Spring IoC、DI与构造方法注入CGLIB、Byte Buddy 与 Spring AOPSpring 常用注解详解Spring Bean 生命周期
Spring MVC异常处理机制
linux常用命令
mac快捷键终端常用操作
python
基础01.Python是什么,为什么要安装Python02.Python项目中.venv是什么03.Python基础语法
基础实战sql替换
HTTPXHRMS员工详情接口调用(Python HTTPX)
windowswindows激活

06-ThreadLocal

返回 JUC 导航

Local

基本介绍

ThreadLocal 类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过 get 和 set 方法访问)时能保证各个线程的变量相对独立于其他线程内的变量,分配在堆内的 TLAB 中

ThreadLocal 实例通常来说都是 private static 类型的,属于一个线程的本地变量,用于关联线程和线程上下文。每个线程都会在 ThreadLocal 中保存一份该线程独有的数据,所以是线程安全的

ThreadLocal 作用:

  • 线程并发:应用在多线程并发的场景下

  • 传递数据:通过 ThreadLocal 实现在同一线程不同函数或组件中传递公共变量,减少传递复杂度

  • 线程隔离:每个线程的变量都是独立的,不会互相影响

对比 synchronized:

synchronizedThreadLocal
原理同步机制采用以时间换空间的方式,只提供了一份变量,让不同的线程排队访问ThreadLocal 采用以空间换时间的方式,为每个线程都提供了一份变量的副本,从而实现同时访问而相不干扰
侧重点多个线程之间访问资源的同步多线程中让每个线程之间的数据相互隔离

基本使用

常用方法
方法描述
ThreadLocal<>()创建 ThreadLocal 对象
protected T initialValue()返回当前线程局部变量的初始值
public void set( T value)设置当前线程绑定的局部变量
public T get()获取当前线程绑定的局部变量
public void remove()移除当前线程绑定的局部变量
public class MyDemo {
 
    private static ThreadLocal<String> tl = new ThreadLocal<>();
 
    private String content;
 
    private String getContent() {
        // 获取当前线程绑定的变量
        return tl.get();
    }
 
    private void setContent(String content) {
        // 变量content绑定到当前线程
        tl.set(content);
    }
 
    public static void main(String[] args) {
        MyDemo demo = new MyDemo();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    // 设置数据
                    demo.setContent(Thread.currentThread().getName() + "的数据");
                    System.out.println("-----------------------");
                    System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
                }
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
}

应用场景

ThreadLocal 适用于下面两种场景:

  • 每个线程需要有自己单独的实例
  • 实例需要在多个方法中共享,但不希望被多线程共享

ThreadLocal 方案有两个突出的优势:

  1. 传递数据:保存每个线程绑定的数据,在需要的地方可以直接获取,避免参数直接传递带来的代码耦合问题
  2. 线程隔离:各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失

ThreadLocal 用于数据连接的事务管理:

public class JdbcUtils {
    // ThreadLocal对象,将connection绑定在当前线程中
    private static final ThreadLocal<Connection> tl = new ThreadLocal();
    // c3p0 数据库连接池对象属性
    private static final ComboPooledDataSource ds = new ComboPooledDataSource();
    // 获取连接
    public static Connection getConnection() throws SQLException {
        //取出当前线程绑定的connection对象
        Connection conn = tl.get();
        if (conn == null) {
            //如果没有,则从连接池中取出
            conn = ds.getConnection();
            //再将connection对象绑定到当前线程中,非常重要的操作
            tl.set(conn);
        }
        return conn;
    }
	// ...
}

用 ThreadLocal 使 SimpleDateFormat 从独享变量变成单个线程变量:

public class ThreadLocalDateUtil {
    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };
 
    public static Date parse(String dateStr) throws ParseException {
        return threadLocal.get().parse(dateStr);
    }
 
    public static String format(Date date) {
        return threadLocal.get().format(date);
    }
}

实现原理

底层结构

JDK8 以前:每个 ThreadLocal 都创建一个 Map,然后用线程作为 Map 的 key,要存储的局部变量作为 Map 的 value,达到各个线程的局部变量隔离的效果。这种结构会造成 Map 结构过大和内存泄露,因为 Thread 停止后无法通过 key 删除对应的数据

JDK8 以后:每个 Thread 维护一个 ThreadLocalMap,这个 Map 的 key 是 ThreadLocal 实例本身,value 是真正要存储的值

  • 每个 Thread 线程内部都有一个 Map (ThreadLocalMap)
  • Map 里面存储 ThreadLocal 对象(key)和线程的私有变量(value)
  • Thread 内部的 Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 map 获取和设置线程的变量值
  • 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成副本的隔离,互不干扰

JDK8 前后对比:

  • 每个 Map 存储的 Entry 数量会变少,因为之前的存储数量由 Thread 的数量决定,现在由 ThreadLocal 的数量决定,在实际编程当中,往往 ThreadLocal 的数量要少于 Thread 的数量
  • 当 Thread 销毁之后,对应的 ThreadLocalMap 也会随之销毁,能减少内存的使用,防止内存泄露

成员变量
  • Thread 类的相关属性:每一个线程持有一个 ThreadLocalMap 对象,存放由 ThreadLocal 和数据组成的 Entry 键值对

    ThreadLocal.ThreadLocalMap threadLocals = null
  • 计算 ThreadLocal 对象的哈希值:

    private final int threadLocalHashCode = nextHashCode()

    使用 threadLocalHashCode & (table.length - 1) 计算当前 entry 需要存放的位置

  • 每创建一个 ThreadLocal 对象就会使用 nextHashCode 分配一个 hash 值给这个对象:

    private static AtomicInteger nextHashCode = new AtomicInteger()
  • 斐波那契数也叫黄金分割数,hash 的增量就是这个数字,带来的好处是 hash 分布非常均匀:

    private static final int HASH_INCREMENT = 0x61c88647

成员方法

方法都是线程安全的,因为 ThreadLocal 属于一个线程的,ThreadLocal 中的方法,逻辑都是获取当前线程维护的 ThreadLocalMap 对象,然后进行数据的增删改查,没有指定初始值的 threadlcoal 对象默认赋值为 null

  • initialValue():返回该线程局部变量的初始值

    • 延迟调用的方法,在执行 get 方法时才执行
    • 该方法缺省(默认)实现直接返回一个 null
    • 如果想要一个初始值,可以重写此方法, 该方法是一个 protected 的方法,为了让子类覆盖而设计的
    protected T initialValue() {
        return null;
    }
  • nextHashCode():计算哈希值,ThreadLocal 的散列方式称之为斐波那契散列,每次获取哈希值都会加上 HASH_INCREMENT,这样做可以尽量避免 hash 冲突,让哈希值能均匀的分布在 2 的 n 次方的数组中

    private static int nextHashCode() {
        // 哈希值自增一个 HASH_INCREMENT 数值
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
  • set():修改当前线程与当前 threadlocal 对象相关联的线程局部变量

    public void set(T value) {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的 ThreadLocalMap 对象
        ThreadLocalMap map = getMap(t);
        // 判断 map 是否存在
        if (map != null)
            // 调用 threadLocalMap.set 方法进行重写或者添加
            map.set(this, value);
        else
            // map 为空,调用 createMap 进行 ThreadLocalMap 对象的初始化。参数1是当前线程,参数2是局部变量
            createMap(t, value);
    }
    // 获取当前线程 Thread 对应维护的 ThreadLocalMap 
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    // 创建当前线程Thread对应维护的ThreadLocalMap 
    void createMap(Thread t, T firstValue) {
        // 【这里的 this 是调用此方法的 threadLocal】,创建一个新的 Map 并设置第一个数据
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
  • get():获取当前线程与当前 ThreadLocal 对象相关联的线程局部变量

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null) {
            // 以当前的 ThreadLocal 为 key,调用 getEntry 获取对应的存储实体 e
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 对 e 进行判空 
            if (e != null) {
                // 获取存储实体 e 对应的 value值
                T result = (T)e.value;
                return result;
            }
        }
        /*有两种情况有执行当前代码
          第一种情况: map 不存在,表示此线程没有维护的 ThreadLocalMap 对象
          第二种情况: map 存在, 但是【没有与当前 ThreadLocal 关联的 entry】,就会设置为默认值 */
        // 初始化当前线程与当前 threadLocal 对象相关联的 value
        return setInitialValue();
    }
    private T setInitialValue() {
        // 调用initialValue获取初始化的值,此方法可以被子类重写, 如果不重写默认返回 null
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        // 判断 map 是否初始化过
        if (map != null)
            // 存在则调用 map.set 设置此实体 entry,value 是默认的值
            map.set(this, value);
        else
            // 调用 createMap 进行 ThreadLocalMap 对象的初始化中
            createMap(t, value);
        // 返回线程与当前 threadLocal 关联的局部变量
        return value;
    }
  • remove():移除当前线程与当前 threadLocal 对象相关联的线程局部变量

    public void remove() {
        // 获取当前线程对象中维护的 ThreadLocalMap 对象
        ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null)
            // map 存在则调用 map.remove,this时当前ThreadLocal,以this为key删除对应的实体
            m.remove(this);
    }

LocalMap

成员属性

ThreadLocalMap 是 ThreadLocal 的内部类,没有实现 Map 接口,用独立的方式实现了 Map 的功能,其内部 Entry 也是独立实现

// 初始化当前 map 内部散列表数组的初始长度 16
private static final int INITIAL_CAPACITY = 16;
 
// 存放数据的table,数组长度必须是2的整次幂。
private Entry[] table;
 
// 数组里面 entrys 的个数,可以用于判断 table 当前使用量是否超过阈值
private int size = 0;
 
// 进行扩容的阈值,表使用量大于它的时候进行扩容。
private int threshold;

存储结构 Entry:

  • Entry 继承 WeakReference,key 是弱引用,目的是将 ThreadLocal 对象的生命周期和线程生命周期解绑
  • Entry 限制只能用 ThreadLocal 作为 key,key 为 null (entry.get() == null) 意味着 key 不再被引用,entry 也可以从 table 中清除
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        // this.referent = referent = key;
        super(k);
        value = v;
    }
}

构造方法:延迟初始化的,线程第一次存储 threadLocal - value 时才会创建 threadLocalMap 对象

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 初始化table,创建一个长度为16的Entry数组
    table = new Entry[INITIAL_CAPACITY];
    // 【寻址算法】计算索引
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // 创建 entry 对象,存放到指定位置的 slot 中
    table[i] = new Entry(firstKey, firstValue);
    // 数据总量是 1
    size = 1;
    // 将阈值设置为 (当前数组长度 * 2)/ 3。
    setThreshold(INITIAL_CAPACITY);
}

成员方法
  • set():添加数据,ThreadLocalMap 使用线性探测法来解决哈希冲突

    • 该方法会一直探测下一个地址,直到有空的地址后插入,若插入后 Map 数量超过阈值,数组会扩容为原来的 2 倍

      假设当前 table 长度为16,计算出来 key 的 hash 值为 14,如果 table[14] 上已经有值,并且其 key 与当前 key 不一致,那么就发生了 hash 冲突,这个时候将 14 加 1 得到 15,取 table[15] 进行判断,如果还是冲突会回到 0,取 table[0],以此类推,直到可以插入,可以把 Entry[] table 看成一个环形数组

    • 线性探测法会出现堆积问题,可以采取平方探测法解决

    • 在探测过程中 ThreadLocal 会复用 key 为 null 的脏 Entry 对象,并进行垃圾清理,防止出现内存泄漏

    private void set(ThreadLocal<?> key, Object value) {
        // 获取散列表
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        // 哈希寻址
        int i = key.threadLocalHashCode & (len-1);
        // 使用线性探测法向后查找元素,碰到 entry 为空时停止探测
        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
            // 获取当前元素 key
            ThreadLocal<?> k = e.get();
            // ThreadLocal 对应的 key 存在,【直接覆盖之前的值】
            if (k == key) {
                e.value = value;
                return;
            }
            // 【这两个条件谁先成立不一定,所以 replaceStaleEntry 中还需要判断 k == key 的情况】
            
            // key 为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,当前是【过期数据】
            if (k == null) {
                // 【碰到一个过期的 slot,当前数据复用该槽位,替换过期数据】
                // 这个方法还进行了垃圾清理动作,防止内存泄漏
                replaceStaleEntry(key, value, i);
                return;
            }
        }
    	// 逻辑到这说明碰到 slot == null 的位置,则在空元素的位置创建一个新的 Entry
        tab[i] = new Entry(key, value);
        // 数量 + 1
        int sz = ++size;
        
        // 【做一次启发式清理】,如果没有清除任何 entry 并且【当前使用量达到了负载因子所定义,那么进行 rehash
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            // 扩容
            rehash();
    }
    // 获取【环形数组】的下一个索引
    private static int nextIndex(int i, int len) {
        // 索引越界后从 0 开始继续获取
        return ((i + 1 < len) ? i + 1 : 0);
    }
    // 在指定位置插入指定的数据
    private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
        // 获取散列表
        Entry[] tab = table;
        int len = tab.length;
        Entry e;
    	// 探测式清理的开始下标,默认从当前 staleSlot 开始
        int slotToExpunge = staleSlot;
        // 以当前 staleSlot 开始【向前迭代查找】,找到索引靠前过期数据,找到以后替换 slotToExpunge 值
        // 【保证在一个区间段内,从最前面的过期数据开始清理】
        for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
            if (e.get() == null)
                slotToExpunge = i;
     
    	// 以 staleSlot 【向后去查找】,直到碰到 null 为止,还是线性探测
        for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
            // 获取当前节点的 key
            ThreadLocal<?> k = e.get();
    		// 条件成立说明是【替换逻辑】
            if (k == key) {
                e.value = value;
                // 因为本来要在 staleSlot 索引处插入该数据,现在找到了i索引处的key与数据一致
                // 但是 i 位置距离正确的位置更远,因为是向后查找,所以还是要在 staleSlot 位置插入当前 entry
                // 然后将 table[staleSlot] 这个过期数据放到当前循环到的 table[i] 这个位置,
                tab[i] = tab[staleSlot];
                tab[staleSlot] = e;
    			
                // 条件成立说明向前查找过期数据并未找到过期的 entry,但 staleSlot 位置已经不是过期数据了,i 位置才是
                if (slotToExpunge == staleSlot)
                    slotToExpunge = i;
                
                // 【清理过期数据,expungeStaleEntry 探测式清理,cleanSomeSlots 启发式清理】
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                return;
            }
    		// 条件成立说明当前遍历的 entry 是一个过期数据,并且该位置前面也没有过期数据
            if (k == null && slotToExpunge == staleSlot)
                // 探测式清理过期数据的开始下标修改为当前循环的 index,因为 staleSlot 会放入要添加的数据
                slotToExpunge = i;
        }
    	// 向后查找过程中并未发现 k == key 的 entry,说明当前是一个【取代过期数据逻辑】
        // 删除原有的数据引用,防止内存泄露
        tab[staleSlot].value = null;
        // staleSlot 位置添加数据,【上面的所有逻辑都不会更改 staleSlot 的值】
        tab[staleSlot] = new Entry(key, value);
     
        // 条件成立说明除了 staleSlot 以外,还发现其它的过期 slot,所以要【开启清理数据的逻辑】
        if (slotToExpunge != staleSlot)
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    }

    private static int prevIndex(int i, int len) {
        // 形成一个环绕式的访问,头索引越界后置为尾索引
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }
  • getEntry():ThreadLocal 的 get 方法以当前的 ThreadLocal 为 key,调用 getEntry 获取对应的存储实体 e

    private Entry getEntry(ThreadLocal<?> key) {
        // 哈希寻址
        int i = key.threadLocalHashCode & (table.length - 1);
        // 访问散列表中指定指定位置的 slot 
        Entry e = table[i];
        // 条件成立,说明 slot 有值并且 key 就是要寻找的 key,直接返回
        if (e != null && e.get() == key)
            return e;
        else
            // 进行线性探测
            return getEntryAfterMiss(key, i, e);
    }
    // 线性探测寻址
    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
        // 获取散列表
        Entry[] tab = table;
        int len = tab.length;
     
        // 开始遍历,碰到 slot == null 的情况,搜索结束
        while (e != null) {
    		// 获取当前 slot 中 entry 对象的 key
            ThreadLocal<?> k = e.get();
            // 条件成立说明找到了,直接返回
            if (k == key)
                return e;
            if (k == null)
                 // 过期数据,【探测式过期数据回收】
                expungeStaleEntry(i);
            else
                // 更新 index 继续向后走
                i = nextIndex(i, len);
            // 获取下一个槽位中的 entry
            e = tab[i];
        }
        // 说明当前区段没有找到相应数据
        // 【因为存放数据是线性的向后寻找槽位,都是紧挨着的,不可能越过一个 空槽位 在后面放】,可以减少遍历的次数
        return null;
    }
  • rehash():触发一次全量清理,如果数组长度大于等于长度的 2/3 * 3/4 = 1/2,则进行 resize

    private void rehash() {
        // 清楚当前散列表内的【所有】过期的数据
        expungeStaleEntries();
        
        // threshold = len * 2 / 3,就是 2/3 * (1 - 1/4)
        if (size >= threshold - threshold / 4)
            resize();
    }
    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 数组为扩容为原来的 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];
        // 统计新table中的entry数量
        int count = 0;
    	// 遍历老表,进行【数据迁移】
        for (int j = 0; j < oldLen; ++j) {
            // 访问老表的指定位置的 entry
            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);
                    // 将数据存放到新表合适的 slot 中
                    newTab[h] = e;
                    count++;
                }
            }
        }
    	// 设置下一次触发扩容的指标:threshold = len * 2 / 3;
        setThreshold(newLen);
        size = count;
        // 将扩容后的新表赋值给 threadLocalMap 内部散列表数组引用
        table = newTab;
    }
  • remove():删除 Entry

    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)]) {
            // 找到了对应的 key
            if (e.get() == key) {
                // 设置 key 为 null
                e.clear();
                // 探测式清理
                expungeStaleEntry(i);
                return;
            }
        }
    }

清理方法
  • 探测式清理:沿着开始位置向后探测清理过期数据,沿途中碰到未过期数据则将此数据 rehash 在 table 数组中的定位,重定位后的元素理论上更接近 i = entry.key & (table.length - 1),让数据的排列更紧凑,会优化整个散列表查询性能

    // table[staleSlot] 是一个过期数据,以这个位置开始继续向后查找过期数据
    private int expungeStaleEntry(int staleSlot) {
        // 获取散列表和数组长度
        Entry[] tab = table;
        int len = tab.length;
     
        // help gc,先把当前过期的 entry 置空,在取消对 entry 的引用
        tab[staleSlot].value = null;
        tab[staleSlot] = null;
        // 数量-1
        size--;
     
        Entry e;
        int i;
        // 从 staleSlot 开始向后遍历,直到碰到 slot == null 结束,【区间内清理过期数据】
        for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();
            // 当前 entry 是过期数据
            if (k == null) {
                // help gc
                e.value = null;
                tab[i] = null;
                size--;
            } else {
                // 当前 entry 不是过期数据的逻辑,【rehash】
                // 重新计算当前 entry 对应的 index
                int h = k.threadLocalHashCode & (len - 1);
                // 条件成立说明当前 entry 存储时发生过 hash 冲突,向后偏移过了
                if (h != i) {
                    // 当前位置置空
                    tab[i] = null;
                    // 以正确位置 h 开始,向后查找第一个可以存放 entry 的位置
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    // 将当前元素放入到【距离正确位置更近的位置,有可能就是正确位置】
                    tab[h] = e;
                }
            }
        }
        // 返回 slot = null 的槽位索引,图例是 7,这个索引代表【索引前面的区间已经清理完成垃圾了】
        return i;
    }
  • 启发式清理:向后循环扫描过期数据,发现过期数据调用探测式清理方法,如果连续几次的循环都没有发现过期数据,就停止扫描

    //  i 表示启发式清理工作开始位置,一般是空 slot,n 一般传递的是 table.length 
    private boolean cleanSomeSlots(int i, int n) {
        // 表示启发式清理工作是否清除了过期数据
        boolean removed = false;
        // 获取当前 map 的散列表引用
        Entry[] tab = table;
        int len = tab.length;
        do {
            // 获取下一个索引,因为探测式返回的 slot 为 null
            i = nextIndex(i, len);
            Entry e = tab[i];
            // 条件成立说明是过期的数据,key 被 gc 了
            if (e != null && e.get() == null) {
                // 【发现过期数据重置 n 为数组的长度】
                n = len;
                // 表示清理过过期数据
                removed = true;
                // 以当前过期的 slot 为开始节点 做一次探测式清理工作
                i = expungeStaleEntry(i);
            }
            // 假设 table 长度为 16
            // 16 >>> 1 ==> 8,8 >>> 1 ==> 4,4 >>> 1 ==> 2,2 >>> 1 ==> 1,1 >>> 1 ==> 0
            // 连续经过这么多次循环【没有扫描到过期数据】,就停止循环,扫描到空 slot 不算,因为不是过期数据
        } while ((n >>>= 1) != 0);
        
        // 返回清除标记
        return removed;
    }

参考视频:https://space.bilibili.com/457326371/


内存泄漏

Memory leak:内存泄漏是指程序中动态分配的堆内存由于某种原因未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果,内存泄漏的堆积终将导致内存溢出

  • 如果 key 使用强引用:使用完 ThreadLocal ,threadLocal Ref 被回收,但是 threadLocalMap 的 Entry 强引用了 threadLocal,造成 threadLocal 无法被回收,无法完全避免内存泄漏

  • 如果 key 使用弱引用:使用完 ThreadLocal ,threadLocal Ref 被回收,ThreadLocalMap 只持有 ThreadLocal 的弱引用,所以threadlocal 也可以被回收,此时 Entry 中的 key = null。但没有手动删除这个 Entry 或者 CurrentThread 依然运行,依然存在强引用链,value 不会被回收,而这块 value 永远不会被访问到,也会导致 value 内存泄漏

  • 两个主要原因:

    • 没有手动删除这个 Entry
    • CurrentThread 依然运行

根本原因:ThreadLocalMap 是 Thread的一个属性,生命周期跟 Thread 一样长,如果没有手动删除对应 Entry 就会导致内存泄漏

解决方法:使用完 ThreadLocal 中存储的内容后将它 remove 掉就可以

ThreadLocal 内部解决方法:在 ThreadLocalMap 中的 set/getEntry 方法中,通过线性探测法对 key 进行判断,如果 key 为 null(ThreadLocal 为 null)会对 Entry 进行垃圾回收。所以使用弱引用比强引用多一层保障,就算不调用 remove,也有机会进行 GC


变量传递

基本使用

父子线程:创建子线程的线程是父线程,比如实例中的 main 线程就是父线程

ThreadLocal 中存储的是线程的局部变量,如果想实现线程间局部变量传递可以使用 InheritableThreadLocal 类

public static void main(String[] args) {
    ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
    threadLocal.set("父线程设置的值");
 
    new Thread(() -> System.out.println("子线程输出:" + threadLocal.get())).start();
}
// 子线程输出:父线程设置的值

实现原理

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);
    }
}

实现父子线程间的局部变量共享需要追溯到 Thread 对象的构造方法:

private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc,
                  // 该参数默认是 true
                  boolean inheritThreadLocals) {
  	// ...
    Thread parent = currentThread();
 
    // 判断父线程(创建子线程的线程)的 inheritableThreadLocals 属性不为 null
    if (inheritThreadLocals && parent.inheritableThreadLocals != null) {
        // 复制父线程的 inheritableThreadLocals 属性,实现父子线程局部变量共享
        this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); 
    }
    // ..
}
// 【本质上还是创建 ThreadLocalMap,只是把父类中的可继承数据设置进去了】
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) {
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                // 调用的是 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++;
            }
        }
    }
}

参考文章:https://blog.csdn.net/feichitianxia/article/details/110495764