ThreadLocal

原理:

每一个Thread对象中都存在着一个ThreadLocalMap,key为threadlocal对象,value为需要缓存的值,可以简单的理解为threadlocal是一个操作线程中ThreadLocalMap的一个工具类。

在线程第一次使用threadlocal时,创建一个threadlocalmap。

索引如何计算的呢,每创建一个新的threadlocal对象,初始为0,然后加一个特别大的整数作为hash值,然后计算桶下标。

扩容:在元素数量大于数组长度的3分之2时,扩容2倍。索引冲突不同于hashmap,使用的开放寻址法,找下一个空闲的位置,放入。

1.为什么ThreadLocal的key要设计成弱引用?而value不设计成弱引用呢?

  • 当开发者使用 ThreadLocal<String> tl = new ThreadLocal<>(); 后,若 tl 被置为 null(不再使用),此时:
    • 如果 ThreadLocalMap 的 key 是强引用:key 会一直引用 ThreadLocal 对象,导致 ThreadLocal 无法被 GC 回收,进而导致 ThreadLocalMap 中对应的 key-value 永远存在(线程存活时),引发内存泄漏。
    • 如果 key 是弱引用 :当 tl 被置为 null 后,ThreadLocal 对象没有其他强引用,GC 会回收 ThreadLocal,此时 ThreadLocalMap 中的 key 会变成 null,后续可以通过 "清理机制"(比如调用 get()/set()/remove() 时)移除这些 null key 对应的 entry,缓解内存泄漏。
  • value 是强引用,是为了保证 ThreadLocal 存储的数据在 "线程未结束、ThreadLocal 未被回收" 时的有效性
    • ThreadLocal 的核心作用是 "为线程存储私有数据",如果 value 是弱引用,当外部没有其他强引用指向 value 时,GC 会直接回收 value,导致线程还在运行时,无法获取到原本存储的数据(数据提前丢失),违背了 ThreadLocal 的设计初衷。
    • 当然,value 是强引用也会带来风险:如果 ThreadLocal 被回收(key 变成 null),但线程还存活,value 会因为被 ThreadLocalMap 强引用而无法被回收,此时需要主动调用 remove() 清理 value,否则仍会内存泄漏(这也是为什么建议使用 ThreadLocal 后主动调用 remove() 的原因)。后面会讲值释放的时机

2.get方法

当get时发现key为null了,顺手将值清理,然后放一个key进去(get的key是啥就放啥)

3.set方法

会使用启发式扫描,清除临近的null key,启发次数与元素个数有关,与是否发现null key有关。

不会不清理也不会全清理

3.remove方法

一般我们使用ThreadLocal都是用static修饰,就算Entry中的ThreadLocal是一个弱引用,但是静态变量对它是一个强引用。所以我们需要调用remove方法,自己用了threadlocal,肯定知道什么时候不用了,不依赖于垃圾回收或者别的清理,自己手动将其remove掉。

应用场景

ThreadLocal 核心价值是为每个线程提供独立的变量副本,让线程间数据隔离、线程内数据共享,避免多线程并发问题的同时简化数据传递。以下是它最核心、最常见的应用场景,附具体使用逻辑和示例:

一、核心应用场景(高频)

1. 存储线程私有上下文(最经典)

场景 :Web 开发中存储「当前登录用户信息」「请求 ID」「租户 ID」等,避免在方法间层层传递参数。原理 :每个请求由独立线程处理,ThreadLocal 存储当前请求的上下文,任意业务方法可直接获取,无需透传。示例

java 复制代码
// 上下文工具类
public class UserContextHolder {
    // 存储当前登录用户ID
    private static final ThreadLocal<Long> USER_ID = new ThreadLocal<>();
    // 存储请求追踪ID
    private static final ThreadLocal<String> REQUEST_ID = new ThreadLocal<>();

    // 设置用户ID
    public static void setUserId(Long userId) {
        USER_ID.set(userId);
    }
    // 获取用户ID
    public static Long getUserId() {
        return USER_ID.get();
    }
    // 必须手动清理,避免内存泄漏
    public static void remove() {
        USER_ID.remove();
        REQUEST_ID.remove();
    }
}

// 拦截器中设置上下文(Spring MVC示例)
public class UserInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 从Token解析用户ID
        Long userId = parseUserIdFromToken(request.getHeader("Token"));
        UserContextHolder.setUserId(userId);
        UserContextHolder.setRequestId(UUID.randomUUID().toString());
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        // 请求结束后清理,关键!
        UserContextHolder.remove();
    }
}

// 业务层直接获取,无需参数传递
@Service
public class OrderService {
    public void createOrder() {
        Long currentUserId = UserContextHolder.getUserId();
        String requestId = UserContextHolder.getRequestId();
        // 业务逻辑...
    }
}

2. 解决线程不安全的工具类问题

场景 :使用非线程安全的工具类(如 SimpleDateFormatRandom),避免多线程共享导致的并发错误。

原理 :每个线程持有独立的工具类实例,无需加锁,性能优于 synchronized示例

java 复制代码
// 替代共享的SimpleDateFormat,避免线程安全问题
public class DateUtils {
    // 每个线程独立的DateFormat实例
    private static final ThreadLocal<SimpleDateFormat> DATE_FORMATTER = 
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static String format(Date date) {
        return DATE_FORMATTER.get().format(date);
    }

    public static Date parse(String dateStr) throws ParseException {
        return DATE_FORMATTER.get().parse(dateStr);
    }
}

3. 数据库连接 / 事务管理

场景 :数据库连接池分配连接时,为每个线程绑定独立的连接,保证事务的原子性(如 JDBC 手动事务、MyBatis 事务)。原理 :线程内的所有数据库操作共用同一个连接,提交 / 回滚时统一处理,避免跨线程混用连接。简化示例

java 复制代码
public class ConnectionHolder {
    private static final ThreadLocal<Connection> CONN_HOLDER = new ThreadLocal<>();
    // 数据库连接池
    private static final DataSource DATA_SOURCE = getDataSource();

    // 获取当前线程的连接(无则创建)
    public static Connection getConnection() {
        Connection conn = CONN_HOLDER.get();
        if (conn == null) {
            conn = DATA_SOURCE.getConnection();
            CONN_HOLDER.set(conn);
        }
        return conn;
    }

    // 提交事务并释放连接
    public static void commit() {
        Connection conn = CONN_HOLDER.get();
        if (conn != null) {
            try {
                conn.commit();
                conn.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            } finally {
                CONN_HOLDER.remove();
            }
        }
    }

    // 回滚事务
    public static void rollback() {
        // 逻辑类似...
    }
}

数据库连接池并非 "必须" 用 ThreadLocal,但结合 ThreadLocal 是实现 "线程绑定连接" 的最优方案 ------ 核心目的是让「同一个线程内的所有数据库操作复用同一个连接 」,保证事务原子性、避免连接混乱,同时简化连接的获取 / 释放逻辑。

1.先明确核心诉求:为什么要给线程绑定连接?

数据库事务的核心要求是:同一个事务内的所有操作(增删改)必须用同一个数据库连接,否则事务无法保证原子性(比如一个操作在连接 A 提交,另一个在连接 B 回滚)。

如果没有 ThreadLocal,直接从连接池拿连接会出现两个问题:

  1. 线程内多次操作可能拿到不同连接 → 事务失效;
  2. 需手动传递连接对象(比如在方法参数里传 Connection)→ 代码耦合度极高。
2.ThreadLocal 适配连接池的核心价值

ThreadLocal 为 "线程 - 连接" 提供了无侵入的绑定能力,完美解决上述问题,具体体现在 3 个方面:

1. 保证线程内连接唯一性(事务的基础)

先搞懂:增删改为什么必须要 Connection?

数据库的所有操作(查 / 增 / 删 / 改)本质都是通过「数据库连接(Connection)」和数据库交互------Connection 就像你和数据库之间的 "专属电话线":

  • 执行查询:conn.createStatement().executeQuery("select * from user") → 靠这条 "电话线" 把 SQL 发过去,结果传回来;
  • 执行增删改:conn.createStatement().executeUpdate("insert into order ...") → 同样要靠这条 "电话线" 发指令。

尤其是增删改通常要在「事务」中执行(比如创建订单时,要扣库存 + 加订单,要么都成、要么都败),而事务是「绑定在 Connection 上」的:

  • 你调用 conn.setAutoCommit(false) 开启手动事务后,这个 Connection 上的所有操作都会归到同一个事务里;
  • 只有调用 conn.commit()/conn.rollback(),才能提交 / 回滚这个 Connection 上的所有增删改操作。

再搞懂:为什么会被迫层层传递 Connection?

假设你不用 ThreadLocal,要保证 "创建订单" 的事务原子性(扣库存 + 加订单用同一个 Connection),代码会写成这样:

java 复制代码
// Service层:创建订单(需要事务)
public class OrderService {
    @Autowired
    private OrderDAO orderDAO;
    @Autowired
    private StockDAO stockDAO;

    public void createOrder(Order order) {
        // 1. 先从连接池拿一个Connection(事务的核心载体)
        Connection conn = ConnectionPool.getConnection();
        try {
            conn.setAutoCommit(false); // 开启手动事务
            
            // 2. 扣库存:必须把这个conn传给DAO,否则DAO会自己拿新连接
            stockDAO.reduceStock(conn, order.getGoodsId());
            // 3. 加订单:同样要传这个conn,保证和扣库存用同一个连接
            orderDAO.addOrder(conn, order);
            
            conn.commit(); // 事务提交
        } catch (Exception e) {
            conn.rollback(); // 事务回滚
        } finally {
            conn.close(); // 归还连接到池
        }
    }
}

// DAO层:扣库存(必须接收上层传的conn)
public class StockDAO {
    public void reduceStock(Connection conn, Long goodsId) throws SQLException {
        // 用上层传的conn执行SQL,而不是自己拿新连接
        String sql = "update stock set num = num -1 where goods_id = ?";
        PreparedStatement ps = conn.prepareStatement(sql);
        ps.setLong(1, goodsId);
        ps.executeUpdate();
    }
}

// DAO层:加订单(同理)
public class OrderDAO {
    public void addOrder(Connection conn, Order order) throws SQLException {
        // 必须用同一个conn执行SQL
        String sql = "insert into order (id, goods_id) values (?, ?)";
        PreparedStatement ps = conn.prepareStatement(sql);
        // ... 赋值+执行
    }
}

核心痛点:如果不用 ThreadLocal,要保证事务内的所有操作共用一个 Connection,就必须把 Connection 作为参数,从 Service→DAO1→DAO2 层层传递 ------ 代码又丑又耦合,一旦漏传,DAO 就会自己从连接池拿新连接,事务直接失效(扣库存成功、加订单失败,数据不一致)。

用 ThreadLocal 后的对比(不用传参):

java 复制代码
// Service层:不用传Connection,ThreadLocal自动绑定
public void createOrder(Order order) {
    Connection conn = ConnectionManager.getCurrentConn(); // 从ThreadLocal拿
    try {
        stockDAO.reduceStock(order.getGoodsId()); // 不用传conn!
        orderDAO.addOrder(order); // 不用传conn!
        conn.commit();
    } catch (Exception e) {
        conn.rollback();
    } finally {
        ConnectionManager.remove(); // 清空ThreadLocal
    }
}

// DAO层:直接从ThreadLocal拿conn,不用接收参数
public class StockDAO {
    public void reduceStock(Long goodsId) throws SQLException {
        Connection conn = ConnectionManager.getCurrentConn(); // 从ThreadLocal拿
        // ... 执行SQL
    }
}
2. 适配线程池,避免连接混用

实际项目中,Web 请求 / 业务任务通常由线程池处理(比如 Tomcat 线程池):

  • ThreadLocal 天然隔离线程池中的不同线程,每个线程只能拿到自己绑定的连接;
  • 即使线程复用,只要在任务结束后调用 remove() 清空 ThreadLocal,就不会出现 "线程 A 拿到线程 B 的连接" 的问题。

先补前提:线程池的核心特点是 "复用线程"(比如 Tomcat 线程池创建 10 个线程,处理 100 个请求,每个线程会被重复用 10 次)。

先想:没有 ThreadLocal,线程池会出什么问题?

假设你用一个全局 Map 存 "线程 - 连接",代码如下:

java 复制代码
// 全局Map:线程→连接(不用ThreadLocal的反面例子)
private static Map<Thread, Connection> THREAD_CONN_MAP = new HashMap<>();

// 获取连接
public static Connection getConn() {
    Thread currentThread = Thread.currentThread();
    // 如果当前线程没有绑定连接,从池里拿一个
    if (!THREAD_CONN_MAP.containsKey(currentThread)) {
        Connection conn = ConnectionPool.getConnection();
        THREAD_CONN_MAP.put(currentThread, conn);
    }
    return THREAD_CONN_MAP.get(currentThread);
}

坑来了 :线程池中的线程 A 处理完请求 1 后,会被放回池里,接着处理请求 2。如果没清空THREAD_CONN_MAP里的 "线程 A - 连接 1",那么:

  • 请求 2 用线程 A 处理时,会拿到请求 1 的连接 1 → 相当于 "线程 A(请求 2)拿到了请求 1 的连接",如果请求 1 的事务还没提交,请求 2 的操作会混入请求 1 的事务,导致数据错乱。

再看:ThreadLocal 为什么能适配线程池?

ThreadLocal 的核心特性是:每个线程有独立的变量副本,线程之间完全隔离,且 "线程级别的隔离" 和 "线程是否复用" 无关。

举个 Tomcat 线程池的实际例子:

  1. 线程池初始化 10 个线程(线程 1~ 线程 10);
  2. 请求 1 过来 → 分配线程 1 → ThreadLocal 为线程 1 绑定连接 1;
  3. 请求 1 处理完 → 调用ThreadLocal.remove() → 线程 1 的 Connection 副本被清空;
  4. 线程 1 回到线程池,处理请求 2 → ThreadLocal 为线程 1 重新绑定连接 2(和连接 1 无关);
  5. 线程 2 处理请求 3 → ThreadLocal 为线程 2 绑定连接 3,线程 1 和线程 2 的副本互不干扰。

关键逻辑

  • 线程复用只是 "线程对象被重复使用",但 ThreadLocal 给每个线程维护的副本是 "跟着线程走的",线程 1 的副本永远只属于线程 1,线程 2 拿不到;
  • 只要在请求 / 任务结束时调用remove(),就能清空当前线程的副本,避免下一次复用线程时,拿到上一次的连接。
一句话总结:

线程池的问题是 "线程会被复用,容易混用上一次的连接";ThreadLocal 的优势是 "线程内副本隔离 + 手动 remove 清空",既保证同一个线程内的连接唯一,又避免线程复用时的连接混用。

3.关键注意点:ThreadLocal 不是连接池的 "必须项",但却是 "最优项"

  • 不用 ThreadLocal 也能实现线程绑定连接(比如手动维护一个 Map<Thread, Connection> ),但:
    1. Map 需加锁保证线程安全,性能低于 ThreadLocal(ThreadLocal 无锁);
    2. Map 无法自动感知线程销毁,容易导致连接泄漏;
    3. 代码复杂度远高于 ThreadLocal。
  • 只有 "无需事务的简单查询"(每次拿连接、用完就还),可以直接从连接池拿连接而不用 ThreadLocal------ 但这类场景只是少数。

总结

数据库连接池 + ThreadLocal 的组合,是 **"连接复用(池)" + "线程内连接唯一(ThreadLocal)"** 的黄金搭配:

  • 连接池解决 "连接创建 / 销毁开销大" 的问题;
  • ThreadLocal 解决 "线程内连接统一、事务原子性、代码解耦" 的问题。

这也是 Spring 事务管理(@Transactional)的底层核心逻辑 ------Spring 正是通过 ThreadLocal 将 Connection 绑定到当前线程,保证事务内所有操作复用同一个连接。

二、扩展场景(偏进阶)

  1. 日志追踪:存储线程级的日志上下文(如 traceId、spanId),让全链路日志能关联到同一个请求;
  2. 缓存隔离:为每个线程维护独立的本地缓存(区别于全局缓存),适用于线程内短期复用的数据;
  3. 测试框架:单元测试中隔离不同测试用例的线程变量,避免测试数据互相干扰。

三、使用注意事项(必看)

  1. 必须手动清理 :线程池复用线程时,ThreadLocal 中的数据不会自动清空,需在任务结束后调用 remove(),否则会导致内存泄漏或数据错乱;
  2. 避免存储大对象:每个线程都持有副本,大对象会占用大量内存;
  3. 不依赖 ThreadLocal 传参:核心业务逻辑尽量通过方法参数传递,ThreadLocal 仅用于上下文类数据,降低耦合。
相关推荐
程芯带你刷C语言简单算法题7 小时前
Day30~实现strcmp、strncmp、strchr、strpbrk
c语言·学习·算法·c
桓峰基因7 小时前
SCS 60.单细胞空间转录组空间聚类(SPATA2)
人工智能·算法·机器学习·数据挖掘·聚类
Tjohn97 小时前
Java环境配置(JDK8环境变量配置)补充
java·开发语言
天赐学c语言7 小时前
12.17 - 合并两个有序数组 && include<> 和 include““ 的区别
c++·算法·leecode
摇摆的含羞草7 小时前
Java加解密相关的各种名词的含义,各种分类的算法及特点
java·开发语言·算法
huohuopro7 小时前
java金额转换,将数字金额转换为7位大写
java·开发语言
lionliu05197 小时前
数据库的乐观锁和悲观锁的区别
java·数据库·oracle
脸大是真的好~7 小时前
JVM面试题相关-中级
jvm