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 内存泄漏问题。
参考: