java基础之ThreadLocal详解
ThreadLocal是面试重灾区,但是好像我没遇到过有人问,尴尬脸,不过我们不能做砧板上的鱼肉静静等待宰割,分为两篇来讲解其中的用法和原理。这是第一篇。
一、ThreadLocal简介
ThreadLocal
类用来提供线程内部的局部变量。这些变量在多线程环境下访问(通过get
或set
方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量,ThreadLocal
实例通常来说都是private static
类型。
ThreadLocal
类提供了四个对外开放的接口方法,这也是用户操作ThreadLocal
类的基本方法:
void set(Object value)
设置当前线程的线程局部变量的值。public Object get()
该方法返回当前线程所对应的线程局部变量。public void remove()
将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。protected Object initialValue()
返回该线程局部变量的初始值,该方法是一个protected
的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()
或set(Object)
时才执行,并且仅执行1次,ThreadLocal
中的缺省实现直接返回一个null
。
一个简单的小例子来感受ThreadLocal到底是什么以及怎么用:
运行结果:
1 | Thread-0 |
分析
可以,看出虽然多个线程对同一个变量进行访问,但是由于threadLocal
变量由ThreadLocal
修饰,则不同的线程访问的就是该线程设置的值,这里也就体现出来ThreadLocal
的作用。
当使用ThreadLocal
维护变量时,ThreadLocal
为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
二、扒开JDK threadlocal神秘面纱
threadlocal的原理图为:
那ThreadLocal
内部是如何为每一个线程维护变量副本的呢?到底是什么原理呢?
先来看一下ThreadLocal
的set()
方法的源码是如何实现的:
我们看到,首先通过getMap(Thread t)
方法获取一个和当前线程相关的ThreadLocalMap
,然后将变量的值设置到这个ThreadLocalMap
对象中,当然如果获取到的ThreadLocalMap
对象为空,就通过createMap
方法创建。
我们再往下面去一点,比如map.set
方法到底是怎么实现的?
结合上面的图,其实我们可以发现,数据并不是放在所谓的Map
集合中,而是放进了一个Entry
数组中,这个entry
索引是上面计算好的,entry
的key
是指向threadLocal
的一个软引用,value
是指向真实数据的一个强引用,以后再获取的时候,再以同样的方式计算得到索引下标即可。
上面代码出现的 ThreadLocalMap 是什么?
ThreadLocalMap
是ThreadLocal
类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解),每个线程中都有一个独立的ThreadLocalMap
副本,它所存储的值,只能被当前线程读取和修改。
我们深入看一下getMap
和createMap
的实现
getMap
:
createMap
:
代码非常直白,就是获取和设置Thread
内的一个叫threadLocals
的变量,而这个变量的类型就是ThreadLocalMap
,这样进一步验证了上文中的观点:每个线程都有自己独立的ThreadLocalMap
对象。
Thread
源码中的threadLocals
:
我们接着看ThreadLocal
中的get
方法如下
- 第一步 先获通过
Thread.currentThread()
取当前线程 - 第二步 然后获取当前线程的
threadLocals
属性 - 第三步 在
threadLocals
属性里获取Entry
实例 - 第四部 从
Entry
实例的value
属性里获取到最后所要的Object
对象
接下来讨论一下上面出现的ThreadLocalMap
类以及Entry
类,直接贴源码
Entry
是ThreadLocalMap
的内部类,而且ThreadLocalMap
里拥有一个类型为Entry[]
的table
属性,而且每个线程实例有自己的ThreadLocalMap
。到这里结论已经很明显了:负责保存ThreadLocal
的key
和value
根本就不是一个Map
类型,而是一个Entry
数组!
Entry
继承WeakReference
,因此继承拥有一个弱引用referent
,而且自身也有一个value
属性。Entry
利用referent
来保存threadLocal
实例的弱引用,利用value
保存Object
的强引用。至于为什么一个是强引用,一个是弱引用,我们在下一篇中来探讨。
最后的问题是怎样在Entry
数组里定位我们需要的Entry
呢?其实上面在set的时候已经大概知道了,现在再来看看代码吧:
留意key.threadLocalHashCode
这个属性,Entry
在保存进Entry[]
数组之前,会利用ThreadLocal
的引用计算出一个hash
值,然后利用这个hash
值作为下标定位到Entry[]
数组的某个位置;
原理总结:ThreadLocal
类并没有一个Map
来保存数据,数据都是保存在线程实例上的;客户端访问ThreadLocal
实例的get
方法,get
方法通过Thread.getCurrentThread
获得当前线程的实例,从而获得当前线程的ThreadLocalMap
对象,而ThreadLocalMap
里包含了一个Entry
数组,里面的每个Entry
保存了ThreadLocal
引用以及Object
引用,Entry
的referent
保存ThreadLocal
的弱引用,Entry
的value
保存Object
的强引用。
三、threadLoca应用
threadlocal
实现的可复用的耗时统计工具Profiler
运行结果:
1 | Thread-0耗时: 1000 |
threadLocal
实现数据库连接线程隔离
通过调用ConnectionManager.getConnection()
方法,每个线程获取到的,都是和当前线程绑定的那个Connection
对象,第一次获取时,是通过initialValue()
方法的返回值来设置值的。通过ConnectionManager.setConnection(Connection conn)
方法设置的Connection
对象,也只会和当前线程绑定。这样就实现了Connection
对象在多个线程中的完全隔离。
在Spring
容器中管理多线程环境下的Connection
对象时,采用的思路和以上代码非常相似。
四、threadLocal缺陷
ThreadLocal
变量的这种隔离策略,也不是任何情况下都能使用的。
如果多个线程并发访问的对象实例只能创建那么一个,那就没有别的办法了,老老实实的使用同步机制吧。
下一篇探讨ThreadLocal
内存泄漏问题。
参考: