并发编程之——ThreadLocal的作用与实现原理

本文深入解析ThreadLocal的工作原理,包括其在多线程环境中的作用、实现方式及潜在的内存泄漏问题。通过实例展示了ThreadLocal如何管理线程私有数据,并讨论了其在数据库连接管理和持久层框架如Mybatis中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

最近项目出了个问题,涉及到ThreadLocal,所以抽时间把这个知识点理一下,以一种更容易理解的方式。不过仔细研究才发现这玩意涉及到的东西真不少,本篇只做概要讲解。

ThreadLocal简介

直接翻译叫线程本地,但是ThreadLocal压根就不是线程本地属性,线程本地属性叫threadLocals,threadLocals通过存储键值对的方式存储线程私有数据(通常是业务对象),它的类型是ThreadLocal.ThreadLocalMap内部类,而ThreadLocal对象则是ThreadLocalMap的Key,它实际上是用于管理线程私有数据对象的Map,具体什么样呢?

public class XXXContext {
    // 1
    public static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>();

    // 2
    public static ThreadLocal<Session> sessionHolder= new ThreadLocal<Session>();

}

来看一张图: 

可以看到,属性threadLocals是存在线程私有内存上的,而Key的值则是静态的ThreadLocal对象。这样的结构往下将能单独开一篇文章,先按下不表。

ThreadLocal的作用

无需讲的冠冕堂皇,最大的用处是:线程随时需要用一个对象去做一些事情,这个对象又不是公共的,而是线程私有的,线程又不想哪哪都带着它,只是想什么时候用到,随手就可以招过来。

这样的解释你一定看得懂,但是这只算是表象,标准完整的解释是:Java提供ThreadLocal用于解决这样一个问题,复杂业务流程中的某些数据对象在整个业务流程中处处都有使用,为了降低参数传递的复杂性(说白了就是不想把这样的数据对象作为参数进行传递调用),线程提供了私有属性threadLocals(ThreadLocal.ThreadLocalMap)以键值对的形式存储这样的数据,但是因为这样的数据可能导致内存泄露,因而提供了ThreadLocal类来协调处理这些线程私有数据。

不知道这样的解释你能否看懂,反正我说的够累的。

那么都有哪些对象符合上面的描述呢? 首先想到的就是数据库连接,一个复杂业务,可能有很多次数据库交互,R&D写代码如果哪哪都带着这个连接,那代码就显得很不简练,因而就需要把数据库连接放到线程私有属性上去,数据结构是一个键值对,其中Key是ThreadLocal对象,Value就是数据库连接或者连接的包装类;

现如今,持久层框架已经很成熟了,也不需要R&D自行去写连接层的代码,比如:Mybatis,其本质上也是用的ThreadLocal;

/**
 * @author Larry Meadors
 */
public class SqlSessionManager implements SqlSessionFactory, SqlSession {

  private final SqlSessionFactory sqlSessionFactory;
  private final SqlSession sqlSessionProxy;

  private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<SqlSession>();

  private SqlSessionManager(SqlSessionFactory sqlSessionFactory) {
    this.sqlSessionFactory = sqlSessionFactory;
    this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[]{SqlSession.class},
        new SqlSessionInterceptor());
  }

  //。。。。。。
}

ThreadLocal的实现方式

ThreaLocal有个静态内部类ThreadLocalMap,它就是线程私有属性的类型。

看下面代码,ThreadLocalMap核心内容是:

  1. INITIAL_CAPACITY:初始化数组大小
  2. Entry[]:Entry对象数组
  3. size:数组当前数据量
  4. threshold:扩容阈值

是不是很熟悉,就是一简化版的HashMap

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

        /**
         * The number of entries in the table.
         */
        private int size = 0;

        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0

set逻辑

set逻辑很简单,先取Key值得hashcode,与数组的length-1做与运算,得到落点index,如果这个落点已经被占了,就往后移一位,如果当前落点已经是最后的空位且被占用,则从0开始,直到找到空位为止;找到完空位以后,就把Value放下去,放完以后还要检验数组的使用量是否超过了阈值,即:长度的2/3,如果超过了,还要对数组做扩容操作,扩容的规则同HashMap的数组扩容规则差不多,都是长度乘2,然后做rehash。——这段逻辑描述也是简化版的,真实的逻辑还要复杂的多,而且有它必须复杂的原因。

ThreadLocal的缺点

很多人提到内存泄露,说实在的我觉得这个缺点不能安在ThreadLocal头上,因为这是线程自己的缺陷,而ThreadLocal只是在帮助线程尽量解决整个问题,这个需要单独开篇分析。

1、上面分析set方法有可能出现落点被占用的情况,这种情况因为ThreadLocalMap不使用落点链表,所以它只能依次向后找空落点,当然这个不能说是缺点,因为ThreadLocal绝大多数情况下达不到必须的数据规模;但是在get的时候则存在一定的缺点,那就是当落点不是当前key值,同样需要依次向后找,最坏的情况下,get的时间复杂度是O(n*2/3),所以,ThreadLocal完全可以记录Key值与最终落点的关系,这样复杂度就可以降为O(1)了。

2、最开始提到了,最近一个项目碰到了问题,这个问题就是ThreadLocal使用不当导致的,当然,这个也不能严格算是ThreadLocal的缺点。

什么场景呢?就是ThreadLocal+数据库连接池,问题出现的场景是,数据库是多路由的,而连接池中的连接包含路由信息并且还会被复用,这样一来,当一个连接被复用的时候,后面线程要操作的数据库可能不是它想要的数据库;

举个例子,线程A使用连接1连接A库,完事以后把连接1放回连接池,线程2过来了,复用连接1,这样线程2也会去操作A库,但实际上线程B要操作的是B库,所以业务报错,可能是找不到数据等等。

那解决的方法是:在使用ThreadLocal+连接池的时候,如果数据是线程共享的(比如上面例子中的路由信息),一定要在使用完主动调用remove方法,清除共享数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值