看了网上的博客发现对这个类的作用争议比较大,于是自己看了源码,比较认同的说法是:
This class provides thread-local variables. These variables differ from
their normal counterparts in that each thread that accesses one (via its
{@code get} or {@code set} method) has its own, independently initialized
copy of the variable. {@code ThreadLocal} instances are typically private
static fields in classes that wish to associate state with a thread (e.g.,
a user ID or Transaction ID).
可以总结为一句话:ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
一、基本操作
1、构造函数
1 | public ThreadLocal() { |
没有任何实现
2、initialValue方法
用来设置当前线程的初始值
1 | protected T initialValue() { |
默认返回的是null,声明为protect说明为了让子类重写,一般自己定义的都会重写这个方法来设置初始值。
通常该方法只会被调用一次
- 在调用get方法的时候会第一次调用
- 如果开始就调用了set方法,那么该方法就不会被调用
- 在remove方法之后还会重复上面两条原则
3、get方法
返回当前线程的ThreadLocal值
1 | public T get() { |
4、set方法
用于设置当前线程的ThreadLocal值
1 | public void set(T value) { |
5、remove方法
用来将当前线程的ThreadLocal绑定的值删除
1 | public void remove() { |
在某些情况下需要手动调用该方法,防止内存泄露。
6、使用示例
1 | public class ThreadLocalDemo { |
输出
1 | 线程: 1初始值: 0 |
总结了一下发现其实这个类就是相当于定义一个变量,以上面的为例
如果直接定义一个Integer value变量
1 | private static Integer value=new Integer(0); |
那么所有线程就可以共享这个变量了。
而如果想把这个变量设置为线程间内的局部变量,就可以
1 | private static final ThreadLocal<Integer> value=new ThreadLocal<Integer>(){ |
其实都是定义了一个初始值为0的变量,只不过作用域不同而已。
二、源码分析
关于内部实现网上有很多争议,最开始理解的版本可能比较容易
每个ThreadLocal类创建一个Map,然后用线程的ID作为Map的key,实例对象作为Map的value,这样就能达到各个线程的值隔离的效果。
不知道早期的类是不是这么实现的,但是在JDK8中的源码确实不是这样实现的。
1、get
1 | //1、get方法 |
可以看出整个get方法的流程:
- 首先获取当前线程
- 根据当前线程获取一个Map
- 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的value e,否则转到5
- 如果e不为null,则返回e.value,否则转到5
- Map为空或者e为空,则通过initialValue方法获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map
2、设计思路
从上面的分析可以看出,ThreadLocl的设计思路是和随开始理解的相反的,是以线程为单位去维护一个map,而吧ThreadLocl当做key,而这个map是ThreadLocal的一个内部类ThreadLocalMap
- Thread类中有一个ThreadLocal.ThreadLocalMap变量threadLocals,用于维护这个线程的所有ThreadLocal变量
- 每次获取某个ThreadLocal变量时,都会获取当前线程的map
- 在map中,ThreadLocal变量作为key去查找,如果不存在,就根据设置的初始值去初始化
这样做的好处在于:
- 这样设计之后每个Map的Entry数量变小了:之前是Thread的数量,现在是ThreadLocal的数量,能提高性能,据说性能的提升不是一点两点(没有亲测)
- 当Thread销毁之后对应的ThreadLocalMap也就随之销毁了,能减少内存使用量。
3、ThreadLocalMap
ThreadLocalMap是ThreadL的一个内部类,关于它有一点就是它的Entry是一个弱引用
1 | static class ThreadLocalMap { |
关于引用的四种类型已经介绍过,弱引用面临着随时被GC的风险.
从上面代码可以看出,Entry是继承WeakReference。即Entry实例本质上就是对ThreadLocal对象的弱引用。只不过,Entry同时还保存了value。
所以关于ThreadLocal印发内存泄漏的说法
如上图,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄露。
但是ThreadLocal在设计的时候会避免这个的发生
- 首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (len-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;
- 如果e为null或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry,否则,如果key值为null,则擦除该位置的Entry,否则继续向下一个位置查询
在这个过程中遇到的key为null的Entry都会被擦除
上面的设计思路依赖一个前提条件:要调用ThreadLocalMap的getEntry方法或者set方法。这当然是不可能任何情况都成立的,所以很多情况下需要使用者手动调用ThreadLocal的remove方法,手动删除不再需要的ThreadLocal,防止内存泄露。所以JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。
三、ThreadLocal在Spring中的应用
1、应用
在网上看了N篇博客说明ThreadLocal在Spring中的应用,发现都是出自一处的,不知道是谁转载谁。
在此主要引用参考地址中第三个的博客
ThreadLocal在Spring中发挥着重要的作用,在管理request作用域的Bean、事务管理、任务调度、AOP等模块都出现了它们的身影,起着举足轻重的作用。
举例子比较多的(没见过其他例子)就是Spring的DAO模版。
我们知道Spring通过各种模板类降低了开发者使用各种数据持久技术的难度。这些模板类都是线程安全的,也就是说,多个DAO可以复用同一个模板实例而不会发生冲突。我们使用模板类访问底层数据,根据持久化技术的不同,模板类需要绑定数据连接或会话的资源。但这些资源本身是非线程安全的,也就是说它们不能在同一时刻被多个线程共享。虽然模板类通过资源池获取数据连接或会话,但资源池本身解决的是数据连接或会话的缓存问题,并非数据连接或会话的线程安全问题。
按照传统经验,如果某个对象是非线程安全的,在多线程环境下,对对象的访问必须采用synchronized进行线程同步。但模板类并未采用线程同步机制,因为线程同步会降低并发性,影响系统性能。此外,通过代码同步解决线程安全的挑战性很大,可能会增强好几倍的实现难度。那么模板类究竟仰仗何种魔法神功,可以在无须线程同步的情况下就化解线程安全的难题呢?答案就是ThreadLocal!
2、举例
TopicDao:非线程安全
1 | public class TopicDao { |
由于①处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造:
1 | class TopicDao { |
这是网上的版本,而我觉得可以通过重写初始化方法来直接新建Connection,不需要你们麻烦
1 | class TopicDao { |
这样,就保证了不同的线程使用线程相关的Connection,而不会使用其他线程的Connection。因此,这个TopicDao就可以做到singleton共享了。
当然,这个例子本身很粗糙,将Connection的ThreadLocal直接放在Dao只能做到本Dao的多个方法共享Connection时不发生线程安全问题,但无法和其他Dao共用同一个Connection,要做到同一事务多Dao共享同一个Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。但这个实例基本上说明了Spring对有状态类线程安全化的解决思路。在本章后面的内容中,我们将详细说明Spring如何通过ThreadLocal解决事务管理的问题。
3、Spring源码中的实现
在Spring技术内幕第五章中,找到了关于JdbcTemplete类的实现,由于源码较多,没有完全分析。但是可以发现内部实现确实是使用了ThreadLocal的。
在JdbcTemplete.excute方法中,获取Connection的代码为
1
2//这里取得数据库的Connection,这个数据库的Connection已经在Spring的事务管理之下
Connection con = DataSourceUtils.getConnection(getDatasource());DataSourceUtils是一个辅助类,Spring通过这个辅助类来对数据的Connection进行管理,比如利用它来完成打开和关闭Connection等。
在DataSourceUtils类中,实现了Connection的相关处理,也可以看见Connection和当前线程的绑定,
1
2/*把对数据库的Connection放到事务管理器中进行管理,这里使用TransactionSynchronizationManager中定义的ThreadLocal变量来和线程绑定数据库连接*/
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
下面就是对conHolder的操作了。
如果在TransactionSynchronizationManager中已经有了与当前线程绑定的数据库连接,那就直接取出来使用。
不然就新打开一个数据库的Connection,并通过TransactionSynchronizationManager和当前线程绑定起来。
可以看出,基本与上面的例子思路一致,只不过实现起来更复杂。