Android源码之ThreaLocal
ThreadLocal提供了一种线程操作数据的思想。以前我以为ThreadLocal的实现就是有一个以线程的name为关键字的HashMap,每次请求数据的时候在这个HashMap中查找数据。今天读完代码,我才明白原来是用一种更巧妙的方法实现的。
下面我们就分析一下ThreadLocal的源码实现,
首先我们先看一下ThreadLocal的相关成员变量以及构造函数:
/**Weakreferencetothisthreadlocalinstance.*/
privatefinalReference<ThreadLocal<T>>reference=newWeakReference<ThreadLocal<T>>(this);
在这里可以将reference理解为关键字,下面我们可以看到reference的用处
privatestaticAtomicIntegerhashCounter=newAtomicInteger(0);
privatefinalinthash=hashCounter.getAndAdd(0x61c88647*2);
hash用于计算hash值,hash值就是为了计算索引地址
构造函数如下:
publicThreadLocal(){}
ThreadLocal的构造函数时一个空函数。
在ThreadLocal类中有一个static的内部类Values.Values类提供了存储和索引具体的值的逻辑
我们也来看一下Values的成员变量以及构造函数:
privatestaticfinalintINITIAL_SIZE=16;
初始的存放数据的数组的大小
privatestaticfinalObjectTOMBSTONE=newObject();
TOMBSTONE是一个占位符,表示一个墓碑
privateObject[]table;
保存数据的数组
privateintmask;
用于计算索引值的掩码
privateintsize;
size表示有效的数据的个数
privateinttombstones;
tombstones译为墓碑,表示索引值的数据无效,但是曾经被使用过的数据的个数
privateintmaximumLoad;
maximumLoad表示最大的装载数,也就是size+tombstones的最大值
privateintclean;
clean记录了下次执行cleanup操作的起始位置
Values只提供了默认构造函数,实现如下:
Values(){
initializeTable(INITIAL_SIZE);
this.size=0;
this.tombstones=0;
}
privatevoidinitializeTable(intcapacity){
this.table=newObject[capacity*2];
this.mask=table.length-1;
this.clean=0;
this.maximumLoad=capacity*2/3;//2/3
}
构造函数只是提供了成员变量的初始化。在实现中我们看到,将mask掩码初始化为table的长度-1,这样在计算索引值时,保证索引值落在table里面。将maximumLoad初始化为table的长度的2/3.
(1)set函数
接下来我们看一下ThreadLocal的set函数的实现,set函数将值设置到线程中去
publicvoidset(Tvalue){
//得到当前的运行线程
ThreadcurrentThread=Thread.currentThread();
//获得运行线程的Values值
Valuesvalues=values(currentThread);
//如果values值为空,则说明还没有为该线程设置过值,这时需要初始化一个空的values对象
if(values==null){
values=initializeValues(currentThread);
}
//设置值
values.put(this,value);
}
values函数的实现如下:
Valuesvalues(Threadcurrent){
returncurrent.localValues;
}
我们可以看到,values函数就是返回了线程的localValues成员变量。
在Thread类中,存在ThreadLocal.ValueslocalValues的成员变量,有着包访问权限。从这里我们可以看出,ThreadLocal的值是绑定到各个线程中去的,而不是在ThreadLocal里统一管理。
initializeValues函数的实现也很简单,如下:
ValuesinitializeValues(Threadcurrent){
returncurrent.localValues=newValues();
}
只是简单的初始化Thread的localValues成员变量new一个新Values对象。
接下来我们分析一下Values类的put函数的实现:
voidput(ThreadLocal<?>key,Objectvalue){
//一上来就执行cleanUp操作
cleanUp();
//记录第一个墓碑的位置
intfirstTombstone=-1;
//遍历表
for(intindex=key.hash&mask;;index=next(index)){
//得到关键字
Objectk=table[index];
//查找到关键字,则替换值
if(k==key.reference){
//Replaceexistingentry.
table[index+1]=value;
return;
}
//如果当前位置的关键字是空,说明查找到一个没有使用过的位置,这时候就必须将该值保存
if(k==null){
//firstTombstone==-1,说明在查找过程中没有遇到墓碑,则将该值保存在这个null位置
if(firstTombstone==-1){
//Fillinnullslot.
//保存key和value
table[index]=key.reference;
table[index+1]=value;
//有效的数据个数加1
size++;
return;
}
//firstTombstone!=-1,说明查找过程中遇到过墓碑,则要将该值替换遇到的第一个墓碑
//保存key和value
table[firstTombstone]=key.reference;
table[firstTombstone+1]=value;
//墓碑的个数减1
tombstones--;
//有效的个数加1
size++;
return;
}
//保存第一个墓碑的位置
if(firstTombstone==-1&&k==TOMBSTONE){
firstTombstone=index;
}
}
}
从put的实现逻辑中,我们看到Values是想一个位置保存key,在这个位置的下一个位置处保存value。这就要求table的长度必须为2的整数倍,并且计算索引的步长为2。
我们从next函数的视线中验证了这一点:
privateintnext(intindex){
return(index+2)&mask;
}
我们接下来看一下cleanUp函数的实现逻辑:
privatevoidcleanUp(){
if(rehash()){
//如果重新hash了table,则没有必要在进行cleanUp操作
return;
}
//size=0,表示还没有保存有效的数据
if(size==0){
//Noliveentries==nothingtoclean.
return;
}
//从上次执行cleanUp结束时的索引值开始
intindex=clean;
Object[]table=this.table;
//遍历table
for(intcounter=table.length;counter>0;counter>>=1,index=next(index)){
//得到key
Objectk=table[index];
if(k==TOMBSTONE||k==null){
continue;//ontonextentry
}
@SuppressWarnings("unchecked")
Reference<ThreadLocal<?>>reference=(Reference<ThreadLocal<?>>)k;
//如果为null,表示此对象已经被回收
if(reference.get()==null){
//将该位置设置成墓碑,并将值设置为null
table[index]=TOMBSTONE;
table[index+1]=null;
//增加墓碑的数目和减少有效的对象数目
tombstones++;
size--;
}
}
//保存下次cleanUp的其实索引值
clean=index;
}
从源码中可以看出,cleanUp主要清理了被垃圾回收器回收的对象。这样能保证每次取数据和设置数据时索引位置的有效性。
我们看一下rehash的函数实现:
privatebooleanrehash(){
//如果没有达到可装载的最大值,则不用重新hash,直接返回false
if(tombstones+size<maximumLoad){
returnfalse;
}
intcapacity=table.length>>1;
intnewCapacity=capacity;
//如果一半的容量都被有效的值所占据,则将容量扩大一倍。因为table是将关键字和值保存在相邻的位置上,所以这里其实比较的是table数组的长度的1/4
if(size>(capacity>>1)){
newCapacity=capacity*2;
}
Object[]oldTable=this.table;
//重新分配一个新的table数组
initializeTable(newCapacity);
this.tombstones=0;
//如果有效的值的个数为0,则没必要进行新旧table的赋值
if(size==0){
returntrue;
}
//进行新旧table的赋值
for(inti=oldTable.length-2;i>=0;i-=2){
Objectk=oldTable[i];
if(k==null||k==TOMBSTONE){
//Skipthisentry.
continue;
}
//Thetablecanonlycontainnull,tombstonesandreferences.
@SuppressWarnings("unchecked")
Reference<ThreadLocal<?>>reference=(Reference<ThreadLocal<?>>)k;
ThreadLocal<?>key=reference.get();
if(key!=null){
//该值是有效的,则加入到新的table中
add(key,oldTable[i+1]);
}else{
//该关键字对象已经被回收
size--;
}
}
returntrue;
}
add函数是将值赋值到table数组中。在遍历table的过程中,忽略掉当前索引位置的关键字是墓碑的情况,保证插入到null的位置
voidadd(ThreadLocal<?>key,Objectvalue){
for(intindex=key.hash&mask;;index=next(index)){
Objectk=table[index];
if(k==null){
table[index]=key.reference;
table[index+1]=value;
return;
}
}
}
(2)get()函数
get()函数负责取出已经保存的值。实现如下:
publicTget(){
//得到当前线程的Values对象
ThreadcurrentThread=Thread.currentThread();
Valuesvalues=values(currentThread);
if(values!=null){
Object[]table=values.table;
//计算索引值
intindex=hash&values.mask;
//如果还没有被垃圾回收
if(this.reference==table[index]){
return(T)table[index+1];
}
}else{
//为当前线程初始化values成员
values=initializeValues(currentThread);
}
//在table里没有查找到
return(T)values.getAfterMiss(this);
}
我们看一下getAfterMiss的实现:
ObjectgetAfterMiss(ThreadLocal<?>key){
//保存当前的数据table
Object[]table=this.table;
//计算索引值
intindex=key.hash&mask;
//如果当前关键字为null,则说明没有保存过该值,则没有继续查找下去的必要
if(table[index]==null){
//初始化一个值
Objectvalue=key.initialValue();
//如果在initialValue过程中,没有改变table
if(this.table==table&&table[index]==null){
//将关键字和值保存
table[index]=key.reference;
table[index+1]=value;
size++;
//执行一次cleanUp操作
cleanUp();
returnvalue;
}
//在initialValue的过程中,改变了table
//put到table中去
put(key,value);
returnvalue;
}
intfirstTombstone=-1;
//继续查找
for(index=next(index);;index=next(index)){
Objectreference=table[index];
//如果关键字相等
if(reference==key.reference){
returntable[index+1];
}
//如果当前索引的关键字为null,则表明当前位置还没有被使用过,则没有必要在查找下去
if(reference==null){
//初始化一个值
Objectvalue=key.initialValue();
//如果在initialValue过程中,没有改变table
if(this.table==table){
//如果我们曾经遇到过墓碑,并且现在那个位置还是个墓碑,则将值赋值到第一个墓碑的位置
if(firstTombstone>-1&&table[firstTombstone]==TOMBSTONE){
table[firstTombstone]=key.reference;
table[firstTombstone+1]=value;
tombstones--;
size++;
//Noneedtocleanuphere.Wearen'tfilling
//inanullslot.
returnvalue;
}
//当前索引的位置还是null,则赋值到当前的位置
if(table[index]==null){
table[index]=key.reference;
table[index+1]=value;
size++;
cleanUp();
returnvalue;
}
}
//在initialValue的过程中,改变了table
put(key,value);
returnvalue;
}
//记录第一个墓碑的位置
if(firstTombstone==-1&&reference==TOMBSTONE){
firstTombstone=index;
}
}
}
在ThreadLocal的实现中,initialValue只是简单的返回一个null,等待子类去实现。
protectedTinitialValue(){
returnnull;
}
至此,我们分析了set和get函数的实现。
ThreadLocal还实现了remove的逻辑,我们接下来来分析一下remove的实现
(3)remove()函数
remove函数的源码如下:
publicvoidremove(){
ThreadcurrentThread=Thread.currentThread();
Valuesvalues=values(currentThread);
if(values!=null){
values.remove(this);
}
}
得到当前线程对象的values成员之后,调用Values的remove函数即可。Values的remove函数实现如下:
voidremove(ThreadLocal<?>key){
cleanUp();
for(intindex=key.hash&mask;;index=next(index)){
Objectreference=table[index];
if(reference==key.reference){
//如果关键字相等,则将该位置标记为墓碑。
table[index]=TOMBSTONE;
table[index+1]=null;
tombstones++;
size--;
return;
}
if(reference==null){
//Noentryfound.
return;
}
}
}
至此,我们完整的分析了ThreadLocal的实现。我们思考一下问题:
为什么要使用一个叫墓碑的占位符呢?
我们知道在table数组中,每一个位置可以存放的对象要么等于null,要么是一个要保存的值。我们只要在按顺序遍历table,总会找到一个位置可以保存数据。如果要删除一个数据,只要将该位置设置为null。这样当查找到一个位置为null的时候,就可以知道这个位置肯定是没有被使用的位置,因此也就没有必要再查找下去了。那为什么还有多引入一个墓碑的对象呢?看似这个墓碑是多余的,其实不然!因为计算每个线程的索引值的时候,每个线程的索引值其实是固定的,每次都会映射到相同的位置。这样有了墓碑这个占位符对象之后,能保证别的对象不会使用这个位置,相当于将这个位置保护起来,以便留给特定的线程去使用。
从ThreadLocal的实现中,我们总结到,通过ThreadLocal.set()到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。各个线程中访问的是不同的对象。