ThreadLocal是面试重灾区,但是好像我没遇到过有人问,尴尬脸,不过我们不能做砧板上的鱼肉静静等待宰割,分为两篇来讲解其中的用法和原理。这是第一篇。

一、ThreadLocal简介

ThreadLocal类用来提供线程内部的局部变量。这些变量在多线程环境下访问(通过getset方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量,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到底是什么以及怎么用:

image

运行结果:

1
2
3
4
5
6
7
Thread-0
张三
李四
王五
Thread-1
Chinese
English

分析

可以,看出虽然多个线程对同一个变量进行访问,但是由于threadLocal变量由ThreadLocal 修饰,则不同的线程访问的就是该线程设置的值,这里也就体现出来ThreadLocal的作用。

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

二、扒开JDK threadlocal神秘面纱

threadlocal的原理图为:

image

ThreadLocal内部是如何为每一个线程维护变量副本的呢?到底是什么原理呢?

先来看一下ThreadLocalset()方法的源码是如何实现的:

image

我们看到,首先通过getMap(Thread t)方法获取一个和当前线程相关的ThreadLocalMap,然后将变量的值设置到这个ThreadLocalMap对象中,当然如果获取到的ThreadLocalMap对象为空,就通过createMap方法创建。

我们再往下面去一点,比如map.set方法到底是怎么实现的?

image

结合上面的图,其实我们可以发现,数据并不是放在所谓的Map集合中,而是放进了一个Entry数组中,这个entry索引是上面计算好的,entrykey是指向threadLocal的一个软引用,value是指向真实数据的一个强引用,以后再获取的时候,再以同样的方式计算得到索引下标即可。

上面代码出现的 ThreadLocalMap 是什么?

ThreadLocalMapThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。

我们深入看一下getMapcreateMap的实现

getMap:

image

createMap:

image

代码非常直白,就是获取和设置Thread内的一个叫threadLocals的变量,而这个变量的类型就是ThreadLocalMap,这样进一步验证了上文中的观点:每个线程都有自己独立的ThreadLocalMap对象

Thread源码中的threadLocals

image

我们接着看ThreadLocal中的get方法如下

image

  • 第一步 先获通过Thread.currentThread()取当前线程
  • 第二步 然后获取当前线程的threadLocals属性
  • 第三步 在threadLocals属性里获取Entry实例
  • 第四部 从Entry实例的value属性里获取到最后所要的Object对象

接下来讨论一下上面出现的ThreadLocalMap类以及Entry类,直接贴源码

image

EntryThreadLocalMap的内部类,而且ThreadLocalMap里拥有一个类型为Entry[]table属性,而且每个线程实例有自己的ThreadLocalMap。到这里结论已经很明显了:负责保存ThreadLocalkeyvalue根本就不是一个Map类型,而是一个Entry数组!

Entry继承WeakReference,因此继承拥有一个弱引用referent,而且自身也有一个value属性。Entry利用referent来保存threadLocal实例的弱引用,利用value保存Object的强引用。至于为什么一个是强引用,一个是弱引用,我们在下一篇中来探讨。

最后的问题是怎样在Entry数组里定位我们需要的Entry呢?其实上面在set的时候已经大概知道了,现在再来看看代码吧:

image

留意key.threadLocalHashCode这个属性,Entry在保存进Entry[]数组之前,会利用ThreadLocal的引用计算出一个hash值,然后利用这个hash值作为下标定位到Entry[]数组的某个位置;

image

原理总结:ThreadLocal类并没有一个Map来保存数据,数据都是保存在线程实例上的;客户端访问ThreadLocal实例的get方法,get方法通过Thread.getCurrentThread获得当前线程的实例,从而获得当前线程的ThreadLocalMap对象,而ThreadLocalMap里包含了一个Entry数组,里面的每个Entry保存了ThreadLocal引用以及Object引用,Entryreferent保存ThreadLocal的弱引用,Entryvalue保存Object的强引用。

三、threadLoca应用

threadlocal实现的可复用的耗时统计工具Profiler

image

运行结果:

1
2
Thread-0耗时: 1000
Thread-1耗时: 1999

threadLocal实现数据库连接线程隔离

image

通过调用ConnectionManager.getConnection()方法,每个线程获取到的,都是和当前线程绑定的那个Connection对象,第一次获取时,是通过initialValue()方法的返回值来设置值的。通过ConnectionManager.setConnection(Connection conn)方法设置的Connection对象,也只会和当前线程绑定。这样就实现了Connection对象在多个线程中的完全隔离。

Spring容器中管理多线程环境下的Connection对象时,采用的思路和以上代码非常相似。

四、threadLocal缺陷

ThreadLocal变量的这种隔离策略,也不是任何情况下都能使用的。

如果多个线程并发访问的对象实例只能创建那么一个,那就没有别的办法了,老老实实的使用同步机制吧。

下一篇探讨ThreadLocal 内存泄漏问题。

参考: