前言:ThreadLocal
是线程内部的存储类,通过它可以实现在每个线程中存储自己的私有数据。即数据存储以后,只能在指定的线程中获取这个存储的对象,而其它线程则不能获取到当前线程存储的这个对象。ThreadLocal
有一个典型的应用场景,即我们在前文中说到的Android线程间通信中的Looper。每一个线程都有一个私有的Looper
对象去处理当前线程的消息队列,有不清楚的同学,可以去上篇文章查看。话不多说,今天我们主要探讨的是ThreadLocal
实现线程存储私有数据的工作原理。
- 上面我们提到,通过
ThreadLocal
能实现在线程中存储的私有数据,下面我们来看一个典型的应用案例,我们在UI线程中执行如下代码:
private void testThreadLocal() {
final ThreadLocal<String> nameLocal = new ThreadLocal<>();
nameLocal.set("我是UI主线程存储的数据");
new Thread(new Runnable() {
@Override
public void run() {
nameLocal.set("我是子线程存储的数据");
//打印出当前线程和其存储的数据
System.out.println(Thread.currentThread() + ":" + nameLocal.get());
}
}).start();
//打印出当前线程和其存储的数据
System.out.println(Thread.currentThread() + ":" + nameLocal.get());
}
上面代码中,我们在主线程中创建了一个nameLocal
对象,并且向里面写入了一个字符串。接下来,我们又开启了一个新的子线程,又向同一个nameLocal
再写入一个数据。按理说,既然nameLocal
都是一个ThreadLocal
对象,因此调用get
方法去获取存入的字符串时,应该是一个相同的字符串。但实际结果是怎样?实际输出结果如下:
03-12 15:49:49.726 19346-19346/? I/System.out: Thread[main,5,main]:我是UI主线程存储的数据
03-12 15:49:49.726 19346-19358/? I/System.out: Thread[Thread-209,5,main]:我是子线程存储的数据
通过打印的结果可以看出:在不同线程中,即使操作的同一个ThreadLocal对象,也能够实现数据的私密存储。但是,我们调用ThreadLocal的set方法的时候,操作的是同一个ThreadLocal对象,而且也没有不同的Key去区分不同的value值,为什么不会覆盖上一次存储的value?调用get方法为什么能获取到当前线程存储的数据?带着上面的两个问题,我们一起走入ThreadLocal的源码世界,一探究竟。
ThreadLocal源码分析:
- ThreadLocal存储值set方法详解
从上面的示例可以看出,我们利用ThreadLocal
保存数据的时候,只需要简单的在ThreadLocal
对象上调用set(value)
方法,即可以实现数据的存储,而且实现线程的区分,怎么实现的?接下来进入ThreadLocal
的set
方法一探究竟:
public void set(T var1) {
//获取调用set方法的当前线程
Thread var2 = Thread.currentThread();
//从线程中获取当前线程中保存的ThreadLocal的存储对象ThreadLocalMap
ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
if(var3 != null) {
//如果以前对ThreadLocalMap进行过初始化,直接保存
var3.set(this, var1);
} else {
//未进行过初始化,调用createMap方法先创建,再保存数据
this.createMap(var2, var1);
}
}
set方法内部实现逻辑非常简便清晰,先获取到当前线程的ThreadLocalMap
对象,再通过这个对象去实现信息的存储。说起来简单,但我们仍然对getMap
干了什么事情?set
方法和createMap
方法怎么进行信息存储?有很大的疑惑,接下来我们一一解析。先看getMap
内部实现逻辑:
//ThreadLocal类中的getMap方法
ThreadLocal.ThreadLocalMap getMap(Thread var1) {
return var1.threadLocals;
}
//Thread类中的threadLocals变量定义,每个线程都有这个变量
ThreadLocalMap threadLocals = null;
//ThreadLocalMap类的定义
static class ThreadLocalMap {
//数组的初始容量
private static final int INITIAL_CAPACITY = 16;
//存储的ThreadLocal数组,我们实现线程间私有化数据,就存放在这个数组中
private ThreadLocal.ThreadLocalMap.Entry[] table;
//数组的大小,不包括空数据
private int size;
//用于存储数组的总容量,包括为空的数据
private int threshold;
}
上边罗列了getMap
方法的实现和它用到的ThreadLocalMap
类的定义。getMap
方法就是从当前线程中获取它的ThreadLocalMap
成员变量而已。关于ThreadLocalMap
,我们现在只要知道它是一个存储数据的对象即可,至于它内部实现机制,我们接下来会详细讲解。
接下来看一下ThreadLocalMap
中关键的set
方法是如何实现数据存储的,源码如下:
private void set(ThreadLocal<?> var1, Object var2) {
//ThreadLocal.ThreadLocalMap.Entry的定义见该方法下,其实它也是ThreadLocal对象,只是是虚引用对象
ThreadLocal.ThreadLocalMap.Entry[] var3 = this.table;
//获取数组的大小
int var4 = var3.length;
//通过按位与运算,获取var1在数组中的存储位置。
//补充(&类似于取模(%)运算,但是效率比%高很多,a%b可以用位运算计算:a&b-1)
int var5 = var1.threadLocalHashCode & var4 - 1;
//从数组中取出ThreadLocal对象,相当于遍历数组中存储的数据,只要取出来的数据不为空,就一直通过nextIndex方法获取下一个位置的对象。一开始的时候,数组为空,不会执行for循环中的代码。
for(ThreadLocal.ThreadLocalMap.Entry var6 = var3[var5]; var6 != null; var6 = var3[var5 = nextIndex(var5, var4)]) {
//当数组不为空的时候,进入循环遍历数组操作,nextIndex方法就是取下一个位置
//获取到从数组中取出来的那个ThreadLocal对象
ThreadLocal var7 = (ThreadLocal)var6.get();
//如果以前通过var1对象存储过数据,只更新其值,结束方法(var即是调用set方法的那个ThreadLocal对象)
if(var7 == var1) {
var6.value = var2;
return;
}
//遍历完数组,没有找到以前通过var1存储过数据的痕迹,就把数据存储到数组第一个为null的位置.结束方法
if(var7 == null) {
this.replaceStaleEntry(var1, var2, var5);
return;
}
}
//当数组为空的时候,直接创建一个节点,并且添加到数组中
var3[var5] = new ThreadLocal.ThreadLocalMap.Entry(var1, var2);
//下面的操作是对数组进行重新计算操作
int var8 = ++this.size;
if(!this.cleanSomeSlots(var5, var8) && var8 >= this.threshold) {
this.rehash();
}
}
//Entry 对象的定义,比ThreadLocal多了一个value对象
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> var1, Object var2) {
super(var1);
this.value = var2;
}
}
最后,我们看一下通过ThreadLocal
保存数据时,整个调用链:
//创建ThreadLocal对象
ThreadLocal<String> nameLocal = new ThreadLocal<>();
//调用set方法保存数据
nameLocal.set("value");
//获取当前线程保存的ThreadLocalMap 对象
Thread var2 = Thread.currentThread();
ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
//调用set方法或createMap保存数据
var3.set(this, var1);//存在同一个ThreadLocal对象
或
this.createMap(var2, var1);//不存在同一个ThreadLocal对象
//最后就是向table数组中添加新的节点或更新旧结点
调用我们上面分析的set方法:
//当数组为空时,直接调用如下代码添加节点
var3[var5] = new ThreadLocal.ThreadLocalMap.Entry(var1, var2);
//当数组不为空时,循环更新或添加。更新为var6.value = var2覆盖其值,添加是调用如下方法
this.replaceStaleEntry(var1, var2, var5);
ThreadLocal存储值get方法详解
相比通过
set
方法保存数据来说,获取数据的get
方法就要简便得多。我们一般是通过调用nameLocal.get()
来获取数据,我们就先看一下ThreadLocal
类的get
方法是怎么获取数据的。方法实现如下:
public T get() {
//首先还是拿到当前线程关联的ThreadLocalMap对象
Thread var1 = Thread.currentThread();
ThreadLocal.ThreadLocalMap var2 = this.getMap(var1);
if(var2 != null) {
//从数组中取到指定位置(即通过hashcode和数组大小计算出的,和上面存储时获取var5类似)的那个Entry对象,方法详情见下
ThreadLocal.ThreadLocalMap.Entry var3 = var2.getEntry(this);
if(var3 != null) {
//有数据就直接返回存储的数据
Object var4 = var3.value;
return var4;
}
}
//当没有存储的有数据,就是设置数据为null并且返回。方法实现逻辑会在后面说到。
return this.setInitialValue();
}
ThreadLocalMap
的getEntry
方法是怎么获取到指定ThreadLocal.ThreadLocalMap.Entry
对象的?看一下内部实现:
private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> var1) {
//获取在数组中的位置
int var2 = var1.threadLocalHashCode & this.table.length - 1;
//取当前位置的数据
ThreadLocal.ThreadLocalMap.Entry var3 = this.table[var2];
return var3 != null && var3.get() == var1?var3:this.getEntryAfterMiss(var1, var2, var3);
}
ThreadLocalMap
的setInitialValue
方法又是怎么去设置默认值并返回数据的?看一下它的源码:
private T setInitialValue() {
//this.initialValue()方法就是返回了一个null,因此var赋值为null
Object var1 = this.initialValue();
//获取与当前线程相关联的ThreadLocalMap对象
Thread var2 = Thread.currentThread();
ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
if(var3 != null) {
//上面讲过方法的含义,这里是重写为null值
var3.set(this, var1);
} else {
//上面讲过方法的含义,这里是添加一个为null的新节点
this.createMap(var2, var1);
}
//返回null值
return var1;
}
//上面提到的initialValue()方法源码
protected T initialValue() {
return null;
}
最后,还是看一下完整的get
方法调用链:
//调用set方法从nameLocal中获取数据
nameLocal.get();
//获取当前线程保存的ThreadLocalMap 对象
Thread var2 = Thread.currentThread();
ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
//如果var3为空,就创建一个ThreadLocalMap,并且给数据赋值为null,并返回。调用如下代码
return this.setInitialValue();、
//如果var3不为空,就获取指定位置(通过hashcode计算而来,原理上面讲过)的Entry对象
ThreadLocal.ThreadLocalMap.Entry var3 = var2.getEntry(this);
//返回entry中的数据
Object var4 = var3.value;
return var4;
总结:通过ThreadLocal
实现数据的保存和获取原理到现在已经告一段落了。它是怎么实现数据的线程私有化?其实很简单,主要是通过线程的私有成员变量ThreadLocalMap
实现的,而ThreadLocalMap
中又有一个ThreadLocal.ThreadLocalMap.Entry
[]实现数据的存储。每次我们保存或获取数据都是对这个数组进行操作而已。关于Android多线程的详细讲解,大家可以去 Android多线程相关知识总结 查看。希望能帮助到你额。。。