1.ThreadLocal简介
在 java 线程中,每个线程都有一个 ThreadLocalMap 实例变量(如果不使用 ThreadLocal,不会创建这个 Map,一个线程第一次访问某个 ThreadLocal 变量时,才会创建)。
ThreadLocal很容易让人望文生义,想当然地认为是一个“本地线程”。其实,ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。
它主要由四个方法组成initialValue(),get(),set(T),remove(),其中值得注意的是initialValue(),该方法是一个protected的方法,显然是为了子类重写而特意实现的。该方法返回当前线程在该线程局部变量的初始值,这个方法是一个延迟调用方法,在一个线程第1次调用get()时才执行,并且仅执行1次(即:最多在每次访问线程来获得每个线程局部变量时调用此方法一次,即线程第一次使用get()方法访问变量的时候。如果线程先于get方法调用set(T)方法,则不会在线程中再调用initialValue方法)。ThreadLocal中的缺省实现直接返回一个null:
该 Map 是使用线性探测的方式解决 hash 冲突的问题,如果没有找到空闲的 slot,就不断往后尝试,直到找到一个空闲的位置,插入 entry,这种方式在经常遇到 hash 冲突时,影响效率。
下面我们就具体探讨一下ThreadLocal。
2.ThreadLocal的作用
ThreadLocal的作用主要是做数据隔离,ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。
ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
说到隔离,我们应该不难联系到
事务的隔离
,没错,Spring实现事务隔离采用的就是Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别(@Transaction),巧妙地管理多个事务配置之间的切换,挂起和恢复。
Spring框架里面就是用的ThreadLocal来实现这种隔离,主要是在TransactionSynchronizationManager这个类里面:
值得我们注意的是:
Spring的事务主要是ThreadLocal和AOP去做实现的
除此之外,我们在使用SimpleDataFormat时也会用到,可能你在使用SimpleDataFormat时只是简单的new了一个SimpleDataFormat对象,但是在我们使用SimpleDataFormat的parse()方法时,其方法内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。
解决这个问题最简单的办法就是让每个线程都new 一个自己的 SimpleDataFormat就好了,但是有个很大的问题就是如果我们有1000个线程难道new1000个SimpleDataFormat?
所以我们这个时候就可以利用线程池加上ThreadLocal包装SimpleDataFormat,再调用initialValue让每个线程有一个SimpleDataFormat的副本,从而解决了线程安全的问题,也提高了性能。
你以为只有这么多地方可以用到threadLocal???别着急,如果项目中存在一个线程经常遇到横跨若干方法调用,需要传递的对象,也就是上下文(Context),它是一种状态,经常就是是用户身份、任务信息等,就会存在过渡传参的问题。
如果我们使用类似责任链模式,给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,对象参数就传不进去了,所以我们使用ThreadLocal稍微去做了一下改造,这样只需要在调用前在ThreadLocal中设置参数,其他地方get一下就好了,就像下面那样:
同时,像我们经常使用的cookie,session等数据隔离都是通过ThreadLocal去做实现的。
上面我也提到了ThreadLocal主要是用来做数据隔离使用的,那么它和Synchronized有什么区别呢?
3.ThreadLocal与Synchronized的区别
ThreadLocal其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。
但是ThreadLocal与synchronized有本质的区别:
1、Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
2、Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。
而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
简单来说ThreadLocal,threadlocl是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(threadlocl,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。
4.ThreadLocal的底层实现
ThreadLocal<String> localName = new ThreadLocal();
localName.set("张三");
String name = localName.get();
localName.remove();
上面的代码很简单:在ThreadLocal存放一个元素,然后再去获取它最后再把这个元素给移除,整体来说ThreadLocal也就这个三个基本操作:
set、get、remove
我们依次进源码来看一下:
4.1 set
set做的事很简单:主要就是ThreadLocalMap我们需要重点关注一下,而ThreadLocalMap呢是当前线程Thread一个叫threadLocals的变量中获取的。
看到这儿,我们其实就已经发掘出ThreadLocal数据隔离的真相了。
每个线程Thread都维护了自己的threadLocals变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的threadLocals变量里面的,别人没办法拿到,从而实现了隔离。
上面我提到了一个ThreadLocalMap,ThreadLocalMap底层结构是怎么样子的呢?
4.2 ThreadLocalMap
我们先看看上图所示的源码,
既然有个Map那他的数据结构其实是很像HashMap的,但是看源码可以发现,它并未实现Map接口,而且他的Entry是继承
WeakReference
(弱引用)的,也没有看到HashMap中的next,所以不存在链表了。
我简单说明一下弱引用:弱引用主要应用在不阻止它的key或者value 被回收的mapping,什么意思呢?弱引用的出现就是为了垃圾回收服务的。它引用一个对象,但是并不阻止该对象被回收。如果使用一个强引用的话,只要该引用存在,那么被引用的对象是不能被回收的。弱引用则没有这个问题。在垃圾回收器运行的时候,如果一个对象的所有引用都是弱引用的话,该对象会被回收。
此时就会产生一个问题,没有了链表怎么解决Hash冲突呢?
threadlocalmap 结构就是 entry 数组,我们开发过程中可以一个线程可以有多个TreadLocal来存放不同类型的对象的,但是他们都将放到你当前线程的ThreadLocalMap里,所以肯定要数组来存。
至于具体是如何解决hash冲突的,我们先过一下源码:
从源码里面看到ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个threadLocalHashCode,在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,
int i = key.threadLocalHashCode & (len-1)
。
很明显这是很简单的线性探测法,所以解决hash冲突的方式为
线性探测法
然后会判断一下:如果当前位置是空的,就初始化一个Entry对象放在位置i上。
如果位置i不为空,如果这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value;
如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止。
整体流程如下图所示:
由此,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置,set和get如果冲突严重的话,效率还是很低的。
4.3 get
上图便是get的所有过程
说到这里很多人可能在想ThreadLocal的实例以及其值存放在哪里呢?
在Java中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存,而堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。
但是并不能说ThreadLocal的实例以及其值存放在栈上,虽然threadLocal中值为每个线程所私有,ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。
5.共享线程的ThreadLocal数据
使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。
public void test() {
final ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("ninesun");
Thread t = new Thread() {
@Override
public void run() {
super.run();
System.out.println("获取存放的值:" + threadLocal.get());
}
};
t.start();
}
好了,我们现在知道了使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,但是这些值是怎么在子线程之间进行传递的呢?
传递的逻辑很简单,
上图是我截取的Thead里面代码片段,thread在初始化创建的时候(即构造函数里)有以下操作:
这段代码也很简单,大致意思是:如果线程的inheritThreadLocals变量不为空,而且父线程的inheritThreadLocals也存在,那么我就把父线程的inheritThreadLocals给当前线程的inheritThreadLocals。比如我们上面的例子。
ThreadLocal已经讲了大半了,可是你可能还没意识到问题的严重性,因为上面提到,key是弱引用,而value却是强引用,如果我们在使用threadLocal操作不当时,就会导致一个很严重的后果:
内存泄漏
6.内存泄漏
我们可以看到,ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。
上图便是key被GC以后的场景。
产生上面这种场景的原因来自于
弱引用对象的生命周期
只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
这样说可能不是很直观,我举个简单的例子:
就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。
我们怎么取解决呢?
解决办法太简单了,在代码的最后使用remove就好了,我们只要记得在使用的最后用remove把值清空就好了。
比如,我们之前的代码是:
public void test() {
final ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("ninesun");
}
那么我们就可以通过:
public void test() {
final ThreadLocal threadLocal = new InheritableThreadLocal();
try {
threadLocal.set("ninesun");
} finally {
threadLocal.remove();
}
}
remove的源码也很简单,如上图所示,就是找到对应的值全部置空,这样在垃圾回收器回收的时候,会自动把他们回收掉。
那么问题来了,为啥非得把key设计为弱引用?
7.ThreadLocalMap的key要设计成弱引用?
key不设置成弱引用的话就会造成和entry中value一样内存泄漏的场景。
如果 threadlocalmap 的 key 是强引用, 那么只要线程存在, threadlocalmap 就存在, 而 threadlocalmap 结构就是 entry 数组. 即对应的 entry 数组就存在, 而 entry 数组元素的 key 是 threadLocal。
即便我们在代码中显式赋值 threadlocal 为 null, 告诉 gc 要垃圾回收该对象. 由于上面的强引用存在, threadlocal 即便赋值为 null, 只要线程存在, threadlocal 并不会被回收。
而设置为弱引用, gc扫描到时, 发现threadlocal 没有强引用, 会回收该threadlocal对象。
并且 threadlocal 的 set get remove 都会判断是否 key 为 null, 如果为 null, 那么 value 的也会移除, 之后会被 gc 回收。
ThreadLocal的不足,如解决冲突使用效率最低的线性探测法之类的,可以看看netty的fastThreadLocal来弥补。
《谈谈FastLocal为啥这么快》