这篇文章我们来讲解一下ThreadLocal,关于ThreadLocal不知道大家用的多不多,从名字上看叫做本地线程,其实他并不是一个线程,但是确实和线程有关,他实际上是属于一个线程的变量,其他线程虽然可以访问这个变量,但是这个变量的宿主只属于某个线程,如果这个线程一旦销毁了,这个变量实际上也没什么意义了。
# 引子
可能这样解释的话还是太抽象了,我们从一个简单的需求讲起,一般我们开发多线程程序的时候,最关注的问题之一就是线程安全问题,一般如果多个线程需要共享同一个变量的话我们会用同步锁来处理线程安全问题。但是我们知道一旦加了同步锁,执行效率肯定是有所下降的,那么如果我们想提高效率,但是又想要保证线程安全,那么我们可以想到的是把这个变量在每个线程中复制一份,然后各个线程执行的时候访问的只能是自己的那份变量,这样就没有线程安全问题了,也不需要同步锁了,当然空间上的增加肯定会有一些,但是这起码能换来效率的提升,而且也不会产生线程安全问题,肯定是一种处理多线程问题的有效思路,起码可以多提供一种在不同场景中解决问题的方案,所以肯定是值得考虑的。比如我们看下面这代码,分别在两个线程中计算Test对象的值:
```java
Object object = new Object();
class Test{
int a;
int b;
}
Test test = new Test();
class MyThreadA extends Thread{
@Override
public void run() {
synchronized (object){
test.a = 0;
test.b = 0;
}
}
}
class MyThreadB extends Thread{
@Override
public void run() {
synchronized (object){
test.a = 1;
test.b = 2;
}
}
}
```
这里MyThreadA和MyThreadB是两个不同的线程,他们都可以访问外部的一个Test类的实例。MyThreadA和MyThreadB分别用同步锁来进行test的计算,具体计算的逻辑大家可以忽略,这里只是做个演示。这样确实可以线程安全的修改Test类,但是前面也说了,这样的效率比较低,如果有100个线程可能就需要依次的计算最后再得出结果。前面我们也说了,那么可以分别把Test实例在每个线程中复制一份,这样多线线程可以同时进行计算,至于计算后的结果可能有不同的场景我们这里不多考虑,这里我们仅仅考虑是否能提高多个线程总的计算时间并且同时要保证线程安全。我们修改下代码,让每个线程都拥有自己独立的Test副本:
好,既然这样我们修改下上面代码:
```java
class Test{
int a;
int b;
}
Test testA = new Test();
Test testB = new Test();
class MyThreadA extends Thread{
@Override
public void run() {
testA.a = 0;
testA.b = 0;
}
}
class MyThreadB extends Thread{
@Override
public void run() {
testB.a = 1;
testB.b = 2;
}
}
```
经过上面这样一改,倒是每个线程都独立拥有自己的变量了,但是这里有两个问题,第一,这里声明在外面的两个Test实例是一个公共的对象,随便哪个线程都可以访问,虽然这里我们分别在两个线程中各使用了其中的一个,但是这个是认为自定来确定了,不保证变量多了之后会产生疏忽,把不同变量使用在错误的线程中,我们要求的是每个变量在技术层面上被设计为只能被自己所在线程中访问,而不是依靠人为的来隔离。另一个问题是这里只不过是2个线程,如果有好多线程的话,是不是每个线程都要new一个对象,然后传给每个线程呢,这样是不是太麻烦了,一旦代码逻辑有什么大的改动,那么这里牵扯到的改动也比较大,不符合开闭原则。有同学会说,那好,我把这里变量放到线程里面去呢,比如:
```java
class Test{
int a;
int b;
}
class MyThread extends Thread{
Test test = new Test();
@Override
public void run() {
test.a = 0;
test.b = 0;
}
}
```
以上代码把线程类写成一个,只要new出多个不同的MyThread线程就会执行多个线程。这样看上去好像代码不会增加多少,但是这里第一个问题还是存在,虽然被声明在了线程中,但是在这里线程里面调用了另一个线程,还是可以把这个变量通过参数传递过去访问,所以还是没有做到线程隔离。另外别忘记了,我们这里这个线程的计算方法是非常简单的,因为这里只是一个演示,如果线程里的计算非常复杂,我们可能需要把计算方法抽取出来单独组织成一个方法甚至是一个类,而这里Test变量可能就存在一个方法通用的方法里或者类里面,比如下面代码:
```java
class Test{
int a;
int b;
}
class Execute{
public static void execute(){
Test test = new Test();
test.a = 0;
test.b = 0;
}
}
class MyThread extends Thread{
@Override
public void run() {
Execute.execute();
}
}
```
这里是修改后的代码。可以看到,在MyThread中通过调用Execute类的execute方法来执行计算,这里execute实际可能是个非常复杂的方法,这里主要做个演示,所以看起来比较简单。这里execute方法由于是个共用方法,谁都可以调用,所以线程隔离的问题还是存在。我们现在考虑如果要做到每个线程拥有自己独立的变量,我们可以把new出来的Test对象保存到一个地方,由于这里是在一个公共的方法或者类里面,所以需要一个容器来保存,我们选择一个HashMap来保存他,看下面代码:
```java
class Execute{
static HashMap<Thread,Test> map = new HashMap();
public static void execute(){
Test test = map.get(Thread.currentThread());
if(test == null){
test = new Test();
map.put((Thread.currentThread(),test);
}
test.a = 0;
test.b = 0;
}
}
```
这里我们只看Execute这类,其余类和前面的一样。上面代码我们可以看到会声明一个HashMap,key是一个Thread,value是Test,当我们调用execute方法的时候,如果map中有Test对象则会取出,否则会创建一个Test对象然后保存到map中。这里可能有同学会担心,这个的HashMap声明是static的,会不会多个线程同时操作的时候有线程安全的问题,其实在这个场景中一般不用担心。发生线程安全确实本质有由于对同一个资源进行修改引起的,这里也确实对map进行了修改,但是这里每次修改都是在一个唯一的线程中执行了,也就是说这里Thread.currentThread()的值是唯一的,不可能存在多个线程的Thread.currentThread()是同一个的情况,所以即使这里发现了资源被同时修改的问题,由于key值是唯一的,而Test这里也是new出来的,也是唯一的,所以不会发生线程安全的问题。好了,代码经过这样一修改,我们不同线程只要执行execute这个方法就自动的会根据不同线程访问自己的Test对象了,而且也没用到同步锁,不会降低效率,并且这里把具体的处理方法也抽象出来了,是不是看上去比之前直接写在线程里面简洁很多了,这样线程隔离的问题解决了。
细心的同学可能还发现个问题,这里每个线程里面只能拥有一个Test对象,如果这里execute方法里面要求有两个不同的Test对象来进行计算的话,比如像下面这样:
```java
class Execute{
static HashMap<Thread,Test> map = new HashMap();
public static void execute(){
Test test1 = map.get(Thread.currentThread());
if(test1 == null){
test1 = new Test();
map.put((Thread.currentThread(),test1);
}
test1.a = 0;
test1.b = 0;
Test test2 = map.get(Thread.currentThread());
if(test2 == null){
test2 = new Test();
map.put((Thread.currentThread(),test2);
}
test2.a = 1;
test2.b = 1;
...............
}
}
```
这里有两个Test对象,test1和test2,需要分别计算他们的值,然后再做其他处理。但是由于每个线程只能保存一个Test对象,所以这里计算的其实是同一个对象,test2会把tes1的值给覆盖了,所以这里还需要进行修改,使一个线程里能像正常声明变量那样同时声明多个。我们看下下面的修改:
```java
class Execute{
static HashMap<Thread,HashMap<WrapTest,Test>> map = new HashMap();
public static void execute(){
WrapTest wrapTest1 = new WrapTest();
Test test1 = wrapTest1.get();
if(test1 == null){
test1 = new Test();
wrapTest1.set(test1);
}
test1.a = 0;
test1.b = 0;
WrapTest wrapTest2 = new WrapTest();
Test test2 = wrapTest2.get();
if(test2 == null){
test2 = new Test();
wrapTest2.set(test2);
}
test2.a = 1;
test2.b = 1;
...............
}
}
class WrapTest{
Test get(){
Test test = Execute.map.get(Thread.currentThread()).get(this);
return test;
}
void set(Test test){
Execute.map.get(Thread.currentThread()).put(this,test);
}
}
```
上面这段是修改过的代码,可以看到这里首先会把之前Execute中声明的HashMap修改为HashMap<Thread,HashMap<WrapTest,Test>>,这里value的类型修改为了HashMap<WrapTest,Test>,这里可以通过一个类型为WrapTest的key找到对应的Test对象。WrapTest类定义在后面,我们看到这里在WrapTest中简单定义了两个方法,get和set。这里get方法会从Execute的HashMap中取出当前线程的HashMap<WrapTest,Test>,然后再以本WrapTest为key取出对应的Test对象,这里如果取出的Test是空,可以调用set方法设置到HashMap中。set方法也很简单,先获取本线程的HashMap<WrapTest,Test>,然后以本WrapTest为key,传入的test为value,保存到HashMap中。
通过上面的代码修改,我们看到要想创建一个线程隔离的Test对象,只要new一个WrapTest就可以了,但是创建了WrapTest后,并没有实际创建Test对象,具体对象还是需要通过set方法设置给WrapTest,之后就可以通过调用WrapTest的get方法来获取具体的对象了。这里创建多个WrapTest后,就等于在线程中有多个Test的对象了,至此我们就解决了一个线程中有多个线程隔离的对象了。
可能有些同学还会问题,这里我们都是在说自定义的Test类,如果需要支持其他类怎么办呢?这个其实比较简单,我们把前面和Test相关的地方都改成泛型就可以了,比如我们改成以下这样:
```java
class Execute{
static HashMap<Thread,HashMap<WrapT,T>> map = new HashMap();
public static void execute(){
WrapT<Test> wrapTest1 = new WrapT();
Test test1 = wrapTest1.get();
if(test1 == null){
test1 = new Test();
wrapTest1.set(test1);
}
test1.a = 0;
test1.b = 0;
WrapT<Test> wrapTest2 = new WrapT();
Test test2 = wrapTest2.get();
if(test2 == null){
test2 = new Test();
wrapTest2.set(test2);
}
test2.a = 1;
test2.b = 1;
...............
}
}
class WrapT<T>{
T get(){
T t = Execute.map.get(Thread.currentThread()).get(this);
return t;
}
void set(T t){
Execute.map.get(Thread.currentThread()).put(this,t);
}
}
```
我们把WrapTest类名字改成WrapT并且加入了泛型,然后Execute中的HashMap也把Test类型修改为泛型,这里代码可能写的不严谨,主要我们这里的目的是能有助于理解过程,并非实际开发生产代码,所以大家能理解代码的意思就可以。这样修改后,我们就可以支持其他类型了。
经过这些修改,大致我们已经构建出一个线程隔离的模型了,但是有些同学可能感觉WrapT类中get和set的实现代码写的好像有点"丑陋",这里Execute中声明的HashMap是一个static的虽然能被公共使用,但是这个是和线程相关的写在Execute中有点不伦不类,Execute只不过是个调用方法的类,和保存线程隔离变量没有明确的关系,所以不适合放在这里。并且WrapT中获取这个HashMap也是从Execute中获取,也是有点奇怪,两个类也是没有明确关系的,互相竟然产生了关系,所以我们需要把他们的关系重新修改下。我们再进行如下修改:
```java
class MyThread extends Thread{
HashMap<WrapT,T> map = null;
.............
}
class WrapT<T>{
T get(){
if(Thread.currentThread().map == null){
Thread.currentThread().map = new HashMap();
}
T t = Thread.currentThread().map.get(this);
return t;
}
void set(T t){
if(Thread.currentThread().map == null){
Thread.currentThread().map = new HashMap();
}
Thread.currentThread().map.put(this,t);
}
}
class Execute{
public static void execute(){
WrapT<Test> wrapTest1 = new WrapT();
Test test1 = wrapTest1.get();
if(test1 == null){
test1 = new Test();
wrapTest1.set(test1);
}
test1.a = 0;
test1.b = 0;
WrapT<Test> wrapTest2 = new WrapT();
Test test2 = wrapTest2.get();
if(test2 == null){
test2 = new Test();
wrapTest2.set(test2);
}
test2.a = 1;
test2.b = 1;
...............
}
}
```
以上就是经过修改过的代码。我们看到这里首先会在线程中定义一个HashMap,类型为HashMap<WrapT,T>,这里的key是WarpT,value是具体储存的对象。这样就把之前Execute中定义的HashMap的value放到每个线程中定义了。然后我们看下WrapT的get和set方法也有修改,之前是直接从Execute中获得HashMap,现在修改为从当前进程中获得,如果是第一次使用的话,会创建这个HashMap,之后就可以直接获得了,由于WrapT是属于每个访问它的进程的所以这里WrapT从当前进程中获得HashMap这样是十分合理的,获得HashMap后就和之前的操作类似了,这里就不多说了。最后Execute中调用execute方法还是和之前一样没有什么变化,只不过Execute中现在没有HashMap了,他是纯粹的一个被调用的类,和线程隔离没有什么关系了,这样看上去也非常的舒服。
到这里基本上我们自己写的一个线程隔离的模型就差不多了,虽然十分的简单,但是其实思路以后和java的ThreadLocal非常类似了,如果能理解这个例子,相信对后面的ThreadLocal分析的理解也是十分有帮助的。
最后以上这个例子虽然说每个变量只能被自己的线程访问,但是WrapT类在创建具体的对象的时候也可以保存下来,这样其实间接的也可以被其他线程访问,比如像这样:
```java
class WrapT<T>{
T obj = null;
T get(){
if(obj != null) return obj;
if(Thread.currentThread().map == null){
Thread.currentThread().map = new HashMap();
}
obj = Thread.currentThread().map.get(this);
return obj;
}
void set(T t){
if(Thread.currentThread().map == null){
Thread.currentThread().map = new HashMap();
}
obj = t;
Thread.currentThread().map.put(this,obj);
}
}
```
上面对WrapT进行了一点修改,增加了一个变量obj,用来保存创建的对象,这样的话,如果WrapT被传给其他的线程,其他线程就可以对WrapT中保存的对象进程访问了,当然这还需要根据具体场景来做不同的处理,这里只不过是提出一点思路。
好了,我们介绍的这个例子就到这里,希望能够对后面要分析的ThreadLocal有一些引导作用,下面我们就正式开始分析ThreadLocal的源码。
# ThreadLocal的set和get方法
现在我们开始来看Thread的源码,首先我们看下ThreadLocal这个类的签名:
```java
public class ThreadLocal<T> {
...........
}
```
这个类没有继承任何类,就只有一个泛型,其实我们可以把ThreadLocal看成一个普通的变量,具体的类型由泛型来决定,每new出一个ThreadLocal就等于是new出一个普通的对象,只不过这个对象会在不同线程中存在不同的副本值,而从每个线程的视角来说,好像这个对象是只为自己服务,获取的方式和普通变量区别不大,对开发者来说也十分友好,这就是ThreadLocal让人觉得设计得好的地方。另外,这里我们可以得出一个结论,ThreadLocal是不支持基本类型的,为什么呢?当然泛型不支持是一个基本原因,另外从前面的例子中我们可看出,ThreadLocal的原理是会把一个对象的引用保存在一个容器中,通过容器可以找到保存对象的引用,即地址值,如果要修改的话,根据这个引用就可以修改了。但是基本类型我们知道都是保存的值,而不是引用,所以即是泛型支持也没有有,我们取出了一个基本类型,但是修改的也仅仅是他的副本,对于真正线程中的值没有影响,所以这个ThreadLocal不支持基本类型。好了,下面我们就从他的set方法看起:
```java
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
```
在set方法中,我们看到,首先通过调用getMap方法获取一个ThreadLocalMap,这里调用的方法getMap传入的是Thread,这个方法和我们前面例子中从线程中获取HashMap很类似,我们下面再说这个方法,这里可以暂且看做为一个HashMap,这里返回的是ThreadLocalMap类型。接着如果ThreadLocalMap非空的话,就以当前ThreadLocal为key把参数value保存到ThreadLocalMap,否则如果ThreadLocalMap如果是空的话就需要创建ThreadLocalMap,并保存参数value。
这里保存的方法和文章开头我们介绍的例子差不多,都是把要保存的内容保存到HashMap中。set方法不长,但是里面有2个方法getMap和createMap我们可以进一步进入看下。先看下getMap方法:
```java
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
```
这里可以看到会返回线程Thread的threadLocals字段:
```java
ThreadLocal.ThreadLocalMap threadLocals = null;
```
这个字段就是一个ThreadLocalMap类,这个类等同于我们开始介绍例子中的HashMap,他们都在保存在所在的线程中的,相信理解前面我们讲的例子的话,这里应该也比较好理解。好了,getMap我们大概知道是怎么回事了,我们回到前面set方法中,看下另一个方法createMap:
```java
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
```
createMap方法从名字就可以猜出来,是创建一个ThreadLocalMap,这里创建传入的参数是Thread和一个泛型值,我们可以先说下,这个构造方法在创建ThreadLocalMap的同时,把一个要保存的值会保存在ThreadLocalMap上,所以这里传入了两个值。ThreadLocalMap的构造方法我们在后面分析ThreadLocalMap的时候会一起说,我们前面自己举的例子中是一个HashMap,实际在ThreadLocal中用的不是现成的HashMap,是自定的ThreadLocalMap,这个的分析我们也会在后面进行。这里只要知道createMap方法会创建ThreadLocalMap并保存一个线程本地值就可以了。
好了,看完了set方法,我们在看下get方法:
```java
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
```
get方法和set方法是相对的。这里首先还是获得一个ThreadLocalMap,如果ThreadLocalMap非空的话,会调用ThreadLocalMap的getEntry方法,这个方法同样我们稍等下在看,这里的ThreadLocalMap.Entry类型我们这里可以理解为是一个包装了保存对象的类,从这个类的value字段中可以取出之前保存的ThreadLocal值。这里如果可以取得值的话就返回,否则会调用setInitialValue方法来进行初始化,我们看下setInitialValue这个方法:
```java
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
```
这个方法开始会调用initialValue方法来获取一个初始值,默认的话这里返回的是null,具体可以根据实际场景覆写这个方法返回一个初始值。之后就和set方法类似,先获取当前线程的ThreadLocalMap,如果ThreadLocalMap存在的话,就以ThreadLocal为key,把初始值存入ThreadLocalMap中,否则就调用createMap方法创建ThreadLocalMap,并保存初始值。从这里可以看出这里的初始化可能会保存一个空值在ThreadLocalMap中。
ThreadLocal我们暂时就先看set和get两个方法,其他方法等后面分析好了再回来看,接着我们看看上面一直看到的ThreadLocalMap这个类。
# ThreadLocalMap类
ThreadLocalMap类是ThreadLocal的一个静态内部类,我们先看下他的基本的一些定义:
```java
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 初始容量
private static final int INITIAL_CAPACITY = 16;
// 元素数组
private Entry[] table;
// 当前元素大小
private int size = 0;
// 加载因子,即扩容的大小
private int threshold;
// 加载因子为当前总容量的2/3
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
...............
}
```
上面是ThreadLocalMap一些基础的元素,我们可以看到除了定义了一些基本字段外,这里还有个静态内部类Entry,这个类就代表了保存的元素,他其实是个ThreadLocal的弱引用,我们知道弱引用存储的对象,即使还被其他对象存储着,也能够被GC的回收,这里主要涉及成弱引用的原因是,我们知道ThreadLocal使用的场景主要是在多线程中,而线程存活的时间比较灵活,即使主线程退出了,后台线程也可能会继续存活一段时间,由于ThreadLocal的保存对象都被保存在线程中,所以被线程持有,这样如果是强引用的话就不能被释放,所以这里设置为弱引用,当GC的时候就可以自动被释放。另外Entry还保存了我们具体的需要保存的对象,这个元素不是弱引用的,之所以不用弱引用,是因为每当对ThreadLocalMap进行插入,扩容等操作的时候,都会清理ThreadLocal为空的元素,所以这里的问题不大,我们在后面的代码分析中可以看到。
另外我们看到下面声明了个Entry[]数组,这个数组就是保存具体的每个元素的,初始化时这个数组的大小被设置为16个,后面的threshold字段表示,每当数组容量达到这个大小的时候,需要扩容,也就是会增大数组,这个我们在后面分析到添加方法的时候会在看到。
# ThreadLocalMap构造函数
前面我们在分析ThreadLocal的set和get方法的时候,看到当线程中没有ThreadLocalMap的时候,会new一个ThreadLocalMap,下面我们就看看ThreadLocalMap的构造函数:
```java
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
```
这个构造函数我们前面已经看到过了,会传入一个ThreadLocal和一个具体要保存的对象。这里首先会初始化Entry数组,大小为16个,接着会计算插入这个数组的下标,这里计算的方法是通过计算一个哈希值和容量大小减一做与运算。哈希值的计算我们看下就好:
```java
private static AtomicInteger nextHashCode =
new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
```
具体这里的数学原理我们也不研究了,总之根据我们对于哈希值的理解通过一个特殊因子产生的下标是不容易产生碰撞的,所以这里会计算出一个数字,然后和容器大小做与操作。我们知道数组下标的最大值就是容器大小减一,这里与操作是二进制运算,任何数和容器大小减一的最终值都不可能会超过容器大小,所以这里与操作也可以保证最终下标的计算值在数组大小范围内。
计算出了下标值后,接着new一个Entry,这个我们前面分析过了,他是一个弱引用,保存了一个ThreadLocal对象,同时还保存了一个我们使用的具体对象。构造函数最后把元素大小置1,设置下初始容器大小为16。
# ThreadLocalMap的set方法
之前我们在分析ThreadLocal的get和set方法的时候,有几个方法没有讲,这些方法都是和ThreadLocalMap相关,所以我们先讲ThreadLocalMap相关的概念,现在我们开始说下ThreadLocalMap的set和get方法,这个方法也是之前ThreadLocal的set和get方法最终调用的方法,我们先看下ThreadLocalMap的set方法:
```java
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果有相同ThreadLocal的更新value值
if (k == key) {
e.value = value;
return;
}
// 如果key为null了,说明被GC了,会把数据保存在这个位置
// 并且会清理从这个位置为中心的左右一段距离的key为null的项
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
// 从i这个位置后遍历一段距离,清空一些key为null的项目
// 如果最终清理完元素数量还是大于加载因子,那么就扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash(); // 扩容
}
```
set方法的参数是ThreadLocal和value。通过前面的分析,我们知道最终对象是要保存到Entry数组中的,所以这里ThreadLocal的作用和前面构造方法里面的一样是用来计算数组下标的。计算出数组下标后,便开始遍历当前的Entry数组,由于现在已经计算出了需要插入位置的下标,所以会从这个下标开始遍历。如果找到一个空位置,就会跳出循环,把对象保存在这个位置上。如果这个位置有元素,那么会调用这个元素的get方法,我们知道Entry是一个保存ThreadLocal的弱引用,所以get方法返回的就是一个ThreadLocal,这里会判断是否这个Entry保存的ThreadLocal和当初参数传入的一样,如果一样就说明已经存在了,现在需要更新他的value,所以会把传入的参数覆盖这个Entry的value,然后返回。
之后如果Entry的get出的值是空,说明这个Entry保存的ThreadLocal已经被清理了,我们前面已经看到了Entry是一个WeakReference,所以在GC的时候是有可能被清理的,所以会把当前元素保存在这个位置,我们会调用replaceStaleEntry方法继续清空这个位置左右一定范围内的ThreadLocal为null的数据项,replaceStaleEntry这个方法我们等下回头来看,先继续往下看。
遍历Entry数组如果找到一个空位置,就会跳出循环,把对象保存在这个位置上,然后元素数量加一。之后会调用cleanSomeSlots清理数组中的无效元素,比如像前面遇到的ThreadLocal已经被系统回收的情况,最后会看下当前元素数量是否达到或者超过负载因子,如果超过了还需要调用rehash方法来扩容。这样set方法就算完成了。这个方法里面涉及到了三个方法,replaceStaleEntry,cleanSomeSlots和rehash。
我们先看第一个方法replaceStaleEntry,
```java
// staleSlot这个位置是一个key为null的项,准备把新的Entry插入到这个位置处
// 除了这个之外,会扫描从staleSlot这个位置为中心,左右都直到遇到数组项为null位置的这段数据中
// 是否还有key为null的项,如果有的话,会把这个项置null,同时把Entry的value置null
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
// 从staleSlot往左遍历直至遇到null,看有没有key为null的项,记录下最左的那个位置
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 从staleSlot往右遍历直至遇到null
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 如果slotToExpunge等于staleSlot说明,在staleSlot往左一直到数组项为null
// 都没有key为null的项,后面情况key为null的项只要从slotToExpunge往右就可以了
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 如果遍历到一个key为null的项,并且slotToExpung等于staleSlot,说明staleSlot往左没有key为null的项
// 现在遇到了第一个从staleSlot开始key为null的项,后面情况key为null的项从这个位置往右一直遇到数组项为null就可以了
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 到这里说明数组中没有和这个插入项是同一个ThreadLocal可以更新value的
// 所以会使用传入的staleSlot这个项存放对象
tab[staleSlot].value = null; // 清空原来的value
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
// 经过前面的遍历slotToExpunge保存了从staleSlot左边往右算起第一个key为null的数组下标
// 只有slotToExpunge不等于staleSlot才说明有key为null的数组项,所以需要继续置空
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
```
这个方法咋一看好像有点绕,其实逻辑还算清楚。这里的参数staleSlot是表示这个位置是一个ThreadLocal为null的数组项,所以方法开始从这个位置向左边寻找一个也是ThreadLocal为null的项,直到遇到数组项是null为止,此时slotToExpunge中记录的是左边的一个ThreadLocal为null的数组下标,默认slotToExpunge的值是传入的参数staleSlot。
前面向左扫描仅仅就是记录一个ThreadLocal为null的数组项下标,接着开始向右扫描,同样也是遇到数组项为null为止。在向右扫描的过程中,如果找到一个ThreadLocal和传入的参数一样的项,那么说明找到了,把这项的value值更新为我们传入的值,并且要报这个数据项保存的传入的参数staleSlot这个位置上,因为这个位置是一个ThreadLocal为null的项,所以把这个项保存新的数据,而当前找到的这个项被设置staleSlot上的数据,即是一个ThreadLocal为null的项了。
slotToExpunge变量前面说过是保存着一个从staleSlot往左直到遇到null为止的ThreadLocal为null的项的下标,如果这个值还是staleSlot的话,说明在staleSlot的左边没有ThreadLocal为null的项,而现在staleSlot又被保存为新的数据了,所以把staleSlot更新为当前的i,接着后面会调用expungeStaleEntry和cleanSomeSlots方法,这两个方法都是清空那些ThreadLocal为null的项的,因为这些项其实都是无效项,没必要占用数组空间,并且他们中的value对象还保存在内存中,占用着内容空间,所以也要清楚掉,这两个方法我们稍后看。
向右变量数组直到遇到null为止,此时如果没有找到和保存对象的ThreadLocal一样的数组项,说明当前数组中没有一样的项,所以就new一个Entry保存在staleSlot位置上,并且要把这个位置之前的value置为null。最后如果slotToExpunge不等于staleSlot,说明在以staleSlot为中心,左右都直至遇到null的这段数组中,没有ThreadLocal为null的数组项,那么也就不需要释放什么对象了。如果有的话,也需要调用expungeStaleEntry和cleanSomeSlots方法来清理这些ThreadLocal为null的项。下面我们就看下这两个方法,先看下expungeStaleEntry这个方法:
```java
// 从staleSlot开始往右直到遇到数组项为null,把这段距离的数组中key为null的项都置空
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
// 首先传入的这个参数肯定是null
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
// 开始遍历数组,直到遇到项是null
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
// 获取key
ThreadLocal<?> k = e.get();
if (k == null) { // key为null清空
e.value = null;
tab[i] = null;
size--;
} else {
// 到这里,key不为null。计算这个项应该的数组下标
int h = k.threadLocalHashCode & (len - 1);
// 如果计算出的数组下标和当前所在的数组下标不一样,说明当初存入的时候冲突了
if (h != i) {
// 到这里说明当初存入的时候就是冲突了,所以最好还是存入计算出的位置,先把当前位置置空
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
// 从计算出的应该位置h开始探测,如果是空的就存入
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
```
这个方法传入的参数staleSlot是一个ThreadLocal为null的位置,方法开头先把这个位置的数据清空,同时总的数据总大小减一。接着从下一个位置往后面遍历直至遇到数组项为null为止。期间如果遇到ThreadLocal为null,则清空数组项,数据总大小减一。如果ThreadLocal非null的话,会先计算下这个数据原来应该所在的数据位置,如果计算出的数组位置和当前他所在的位置不一样的话,说明当初插入数组的时候冲突了,所以最终保存的位置改变了,现在既然已经清空了一些数据,还是希望他能保存在原来的位置上,所以这里把当前保存的位置置空,并从原来希望保存的位置h开始遍历,如果是空的话,就把他保存在这个空位置上。最后返回整个数组遍历完直到遇到数组项是null的位置下标。
expungeStaleEntry方法就是清除参数staleSlot开始直到遇到null为止的ThreadLocal为null的数据项,最后他会返回一个null数组的下标,然后会传入上面我们说的另一个方法cleanSomeSlots中,我们再看下cleanSomeSlots这个方法:
```java
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
// 从i这个位置开始往后遍历,直到遇到null
// 如果还有key为null的项清空,i是最后的null的项
// 如果还有下次循环,i从这个遍历后的项开始
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
```
这个方法也是清除那些ThreadLocal为null的项。前面expungeStaleEntry方法的结束条件是遇到一个数组项为null,我们知道这个插入数组的项是通过ThreadLocal的一个哈希计算得出的插入位置,所以数组中有数据和没数据的位置是散列的,他们之间可能是相互交叉存在的,expungeStaleEntry方法中是按照顺序遍历来清除数据项直到遇到null为止,但是可能null的那项后面又是一个ThreadLocal为null的项,所以其实光是依靠expungeStaleEntry方法还不够,这里cleanSomeSlots方法便是继续清除那些可能交叉存在的项。这里传入的参数是一个有效的数组项或者是一个空的数组项,所以这里从参数i的下一项顺序遍历,结束条件就不是遇到数组项为空了,而是根据数组长度,没遍历一次右移一位,直到把所有1的位数全部移出去位置,右移一位等于除以2,其实也就是每次除以2直到为0为止。这样就尽可能的把那些交错的ThreadLocal项也能检查出来,最后如果有检查出这样的项,就返回true。
好了,关于清除ThreadLocal项的方法就说完了,我们回到开始的set方法,前面在分析set方法的时候说是有3个方法,replaceStaleEntry和cleanSomeSlots方法都已经说过了,还有个对数组扩容的方法rehash我们在看一下。
```java
private void rehash() {
expungeStaleEntries(); // 遍历数组清理key为null的项
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4) // 剩余数量大于加载因子
resize();
}
```
这里首先会调用expungeStaleEntries这个方法来清理那些ThreadLocal为空的项,expungeStaleEntries这个方法我们前面已经介绍过了,接着如果剩余的保存对象数量大于3/4的数组加载因子的话,就会调用resize方法将数组扩容,resize方法我们前面也已经看过了,也不多说了。这里要说明下,这里扩容的条件是3/4的加载因子,是因为我们前面分析了清理数组中ThreadLocal为空的项不是完全扫描数组来的,而是以某一个元素为中心扫描他两边的一段距离或者以某个元素为开始往后扫描一定次数,这些方法在上面已经介绍了,还不理解的可以往上看一下。这种不完全扫描的方法可能会导致实际数组中当前的有效元素偏大一些,那么加载因子也自然会偏大一些,所以这里扩容的条件取的是加载因子的3/4,是想将这种偏差稍微降低一些,所以这里条件并不是直接取加载因子,这里作下说明。
# ThreadLocalMap的getEntry方法
好了,ThreadLocalMap的set方法到这里也就基本分析好了,接着我们看下ThreadLocalMap的get方法,ThreadLocalMap没有get方法,只有getEntry方法,其实就是我们理解中的get方法,他的入参是一个ThreadLocal,我们看下这个方法:
```java
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
```
这个方法首先还是计算这个ThreadLocal对应的下标值,然后从Entry数组中取出对应的Entry,如果取出的Entry非空,同时Entry引用的ThreadLocal和入参是同一个,那么就返回这个Entry,否则会调用getEntryAfterMiss方法继续寻找。
```java
// i是要寻找的ThreadLocal的hashCode计算出的数组下标,e是i当前所在的数组元素
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) { // 是空说明没有这个元素,因为查找和插入计算方法是一样的,最终数组null了就说明没有了
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null) // 如果遇到一个key为null的,先清理下
expungeStaleEntry(i);
else // 走到这里说明key不是null,但是也不等于入参ThreadLocal
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
```
这个方法的入参key是需要寻找的ThreadLocal,i是这个ThreadLocal计算出的数组下标i,e是当前这个数组下标所在的元素。这个方法会从i这个位置遍历数组,直到遇到null为止,说明没有找到这个元素。如果在元素非空的情况下,如果ThreadLocal也和入参是一样的,那么说明找到了,返回这个元素。如果ThreadLocal是空的话,说明这个元素被GC了,已经是个无效元素了,所以会调用expungeStaleEntry方法清理这个元素以及他后面的一段数据,这个方法前面分析过了,这里不赘述。如果遍历到的元素ThreadLocal非空但是也和入参的ThreaLocal不相等,那么就继续遍历下一个元素。最终如果遍历到的数组元素为空了,那么就返回null,说明没有找到。
到这里getEntry方法也就分析完了,其实经过前面set方法的分析后,getEntry方法也挺简单的,逻辑思路get和set基本上都是差不多的,都是同一套方法。
# remove方法
经过set和get以及他们相关方法的分析,其实整个ThreadLocal的主要方法也就分析完了,理解了这些基本对ThreadLocal也就基本掌握了。最后我们再来看下remove方法:
```java
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
```
这里是ThreadLocal的remove方法,我们看到他首先还是从当前线程中获取ThreadLocalMap,如果ThreadLocalMap非空的话,会调用ThreadLocalMap的remvoe方法,方法的参数依然是当前这个ThreadLocal。
```java
// 移除入参这个ThreadLocal
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 从i位置往后遍历直到遇到null
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 如果有ThreadLocal相等的,那么下面开始清理
if (e.get() == key) {
// Reference的referent置为null
e.clear();
// 从当前遍历到的i位置往后清理数据
expungeStaleEntry(i);
return;
}
}
}
```
ThreadLocalMap的remove方法也比较简单,这里同样的思路,从需要移除的这个ThreadLocal计算出的数组下标i开始向后遍历,如果有遇到和入参ThreadLocal相同的元素就会调用Entry的clear方法,我们知道Entry是一个WeakReference,WeakReference的父类是Reference,其实就是调用Reference的clear方法,这个方法会清除保存在Reference中的具体对象,从而使Reference的get方法返回变为null,Reference相关的内容也是一个可以细说的知识模块,我们这里也不细说,后面有机会再单独分析,这里就看下这里涉及到的Reference的get和clear方法:
```java
public abstract class Reference<T> {
private T referent;
........
public T get() {
return this.referent;
}
public void clear() {
this.referent = null;
}
.........
}
```
我们这里就给出一小段代码,可以看出这里clear方法把Reference的referent置为null了,所以后面get方法返回的会是null。这里就大致看下Reference涉及的一小段代码,其他的在这里就不多叙述了,我们回到前面remove方法。
在调用了数组元素Entry的clear方法后,接着调用expungeStaleEntry方法,这个方法前面分析过了,会清理那些ThreadLocal为null的数组元素。上面的clear方法就是把Entry的ThreadLocal置为null的,所以这个元素肯定会在expungeStaleEntry方法中被清理掉,这样就等于达到了删除的目的了。如果找到删除元素后,经过上面的删除操作后,就return了,否则就等遍历遇到数组元素为null为止,到这里remove方法也就结束了。
经过上面一系列的ThreadLocal分析,基本上关于ThreadLocal也就都分析完了。ThreadLocal存在的目的,我自己的理解一方面是简化代码的复杂度与冗余性,就好比文章开头所举的例子,如果多个线程共享同一段代码,又要求代码中的变量只能属于执行他的那个线程,这样的需求如果不同ThreadLocal也不是说做不好,但是代码量就比较多了,维护起来也是比较麻烦,而且这种代码就算开发出来了,通用性也不强,可能换一个需求就需要修改很多地方才能复用,所以ThreadLocal的引入给代码带来了更好的通用性。
另一方面ThreadLocal也是对多线程带来了一种以空间换时间的思路,如果多个线程可以不共享同一个资源单独执行的话或者没有绝对的先后执行顺序,那么使用ThreadLocal可以大大提升多线程并发执行的效率,否则如果在任何场景下都使用同步锁的话,那对效率的影响也是挺大的。
到这里,ThreadLocal分析就差不多了,本文开头通过一个例子大致模拟了一个ThreadLocal的使用场景,这个例子对我们理解为何使用ThreadLocal以及使用会带来些什么好处做了一个引导作用。之后就是分享ThreadLocal的源码,从构造函数到get,set方法,最后又看了下remove方法,大致把ThreadLocal的主要内容分析了一遍,相信如果能理解这些内容,那么对于ThreadLocal也能很好的理解了。好了,本文就讲这么多了,我们后面的文章再见。
ThreadLocal源码解析