前言
最近项目出了个问题,涉及到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核心内容是:
- INITIAL_CAPACITY:初始化数组大小
- Entry[]:Entry对象数组
- size:数组当前数据量
- 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方法,清除共享数据。