回顾
通过前面两篇笔记的学习,已经知道了Handler如何使用,Looper的创建和作用,MessageQueue的创建和作用,Message的创建和作用。
-
Handler在主线程中创建,在子线程中发送消息,通过调用sendMessage()和post()系列重载方法来发送消息。发送消息的时候会将需要处理消息的时间一并携带上去,然后根据时间将Message添加到MessageQueue中相应的位置。 -
Looper在主线程创建的时候就会通过调用prepareMainLooper()方法创建,将当前线程标记为一个不可退出的循环线程。同时会创建MessageQueue对象,然后启动loop()方法不断地从MessageQueue中取出需要操作的Message对象,通过它的target属性指定的Handler,调用Handler中对用的方法去处理消息。 -
MessageQueue是一个根据时间创建的消息队列,在Looper中的构造方法执行的时候同时会创建出MessageQueue对象。主要负责消息的入队和出队的操作。 -
Message是消息的实体,表示具体要执行的内容,Message内部通过维护一个对象池来实现Message对象的复用。在从obtain()方法中获取到插入到MessageQueue的这段时间处于没有使用的状态,在插入到MessageQueue到recycleUnchecked()方法回收处于使用中的状态。
这篇笔记主要学习在Looper中拿到Message之后具体是如何处理的。
Handler.dispatchMessage(Message)方法源码
在第一篇笔记中,我们已经了解了,在Looper的loop()方法中执行循环获取下一个Message之后,会通过msg.target.dispatchMessage(msg)来分发当前Message给对应的Handler,下面是这个方法的源码:
/**
* Handle system messages here.
*/
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
- 在这个方法中,首先会判断当前
Message的callback是否为空,通过上一篇笔记可以了解到,Message的callback的类型是Runnable,通过Handler.post(Runnable)系列构造函数发送消息的时候会给Message的callback属性赋值,源码如下:
public final boolean post(Runnable r)
{
return sendMessageDelayed(getPostMessage(r), 0);
}
private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}
- 如果
Message的callback属性不为空,则会执行handCallback(Message)方法,源码如下:
private static void handleCallback(Message message) {
message.callback.run();
}
源码中可以看到,这里也只是执行的Message的callback属性所对应的Runnable的run()方法。
- 如果
Message的callback属性为空,也就是说不是通过post()系列重载方法发送的消息,那么接下里就判断Handler中的mCallback属性是否为空。
mCallback是Handler.Callback类型,这是一个Handler内部的接口,也是用来处理Message信息的。通过查看mCallback的定义final Callback mCallback;也能知道,这个属性只能通过构造函数赋值。所以我们在定义Handler的时候还有另一种方式:
private Handler mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
if(msg.what == 0){
mBinding.tvResult.setText(msg.obj.toString());
}
return false;
}
});
通过这种方式我们就不需要再使用内部类的方式来创建一个Handler,避免了可能出现的内存泄漏的问题。
-
如果
mCallback属性的值不为空,那么就通过这个接口来处理Message,同时查看也能知道,需要注意如果成功处理了Message,那么就要返回true,这个方法默认实现返回false。因为如果处理成功了还返回false,则会继续执行下面的handleMessage(msg)方法,这是没有必要的。 -
如果
mCallback属性的值为空,那么就调用handleMessage(Message)方法来处理Message,这个方法一般需要我们重写它的实现,源码中默认是空的实现:
/**
* Subclasses must implement this to receive messages.
*/
public void handleMessage(Message msg) {
}
至此,Handler的整个处理流程就结束了,如下图所示:

关于Message中障栈的添加
在之前的笔记中有提到过,有一种target为空的Message称为障栈,但是我们自己通过Handler发送的Message都是给target赋值了的,那么MessageQueue中的障栈是如何添加的呢,源码如下:
public int postSyncBarrier() {
return postSyncBarrier(SystemClock.uptimeMillis());
}
private int postSyncBarrier(long when) {
// Enqueue a new sync barrier token.
// We don't need to wake the queue because the purpose of a barrier is to stall it.
synchronized (this) {
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;
Message prev = null;
Message p = mMessages;
if (when != 0) {
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}
在上面的源码中,通过postSyncBarrier()的这个函数和它的重载函数就可以添加一个障栈,添加的方式和普通的添加Message的方式差别不大,唯一的区别就是不用考虑障栈的影响,因为现在要添加的本身就是一个障栈,所以只需要根据它的执行时间的因素将它插入到队列的合适的位置即可。
ThreadLocal
在之前的笔记中,我们了解过,一个线程只能有一个Looper,这是因为在创建Looper对象的时候,我们创建完一个Looper对象,便会将这个对象保存到ThreadLocal中,下一如果还需要创建Looper对象,那么首先会先去检测ThreadLocal中有没有值,如果有,说明当前线程已经创建过一个Looper了,就会抛出异常,源码如下:
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
从上面的源码可以看出,创建线程的时候首先会去ThareadLocal中查找是否有已经创建过的Looper对象,如果有,则会抛出异常,没有则会调用私有的构造方法创建出一个Looper对象,然后把这个对象设置到ThreadLocal中。
那么源码看到这里,就会有以下问题需要解决。
ThreadLocal是什么?有什么作用?
源码中对ThreadLocal的注释是:ThreadLocal类提供了线程局部变量,这些变量与普通变量不同,每个线程都可以通过其get或set方法来访问自己独立的初始化的变量副本。ThreadLocal实例变量通常是类中的privte static字段,它们希望将状态与某一个线程(例如用户ID或事物ID)相关联。
一个简单的例子是这样的:
private ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
private Integer local2 = Integer.valueOf(0);
@Override
protected void initUi() {
threadLocal1.set(100);
local2 = 200;
Log.e("TAG","主线程中的数据:"+threadLocal1.get()+","+local2);
}
@Override
public void doClick(View view) {
super.doClick(view);
if(view.getId() == R.id.btn_get_value){
Log.e("TAG","主线程中的数据:"+threadLocal1.get()+","+local2);
return;
}
new Thread(new Runnable() {
@Override
public void run() {
threadLocal1.set(400);
local2 = 500;
Log.e("TAG","子线程1中的数据:"+threadLocal1.get()+","+local2);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
Log.e("TAG","子线程2中的数据:"+threadLocal1.get()+","+local2);
}
}).start();
}
在上面的代码中,首先在主线程中定义了一个个ThreadLocal<Integer>变量和一个普通的Integer变量,然后给这两个变量在主线程中分别设置值为100和200,在点击事件中启动了两个线程,其中线程1对这两个变量分别设置值为400和500,线程2没有设置值,只是打印数据。当点击另一个按钮的时候会再次在主线程中打印数据,最后打印的数据如下:
2020-03-03 09:59:14.097 4059-4059/com.example.myapplication E/TAG: 主线程中的数据:100,200
2020-03-03 09:59:16.150 4059-4151/com.example.myapplication E/TAG: 子线程1中的数据:400,500
2020-03-03 09:59:16.155 4059-4152/com.example.myapplication E/TAG: 子线程2中的数据:null,500
2020-03-03 09:59:18.955 4059-4059/com.example.myapplication E/TAG: 主线程中的数据:100,500
通过上面打印的数据可以看到,使用ThreadLocal变量的值和线程相关,在哪个线程中设置了什么值,只有在这个线程才能获取到。另外一个线程修改了变量的值也不会对另一个线程中设置的值有影响。但是普通的变量则是所有线程都可以任意修改使用这个变量。
还有一种方式是当我们传递一个ThreadLocal变量会发生什么?
class ThreadTest3 extends Thread{
private ThreadLocal threadLocal;
ThreadTest3(ThreadLocal threadLocal){
this.threadLocal = threadLocal;
}
@Override
public void run() {
super.run();
Log.e("TAG","线程3中的数据:"+threadLocal.get());
}
}
上面的代码运行结果如下:
2020-03-03 10:05:44.042 4261-4261/com.example.myapplication E/TAG: 主线程中的数据:100,200
2020-03-03 10:05:46.747 4261-4338/com.example.myapplication E/TAG: 线程3中的数据:null
可以看到,即便我们将ThreadLocal传递到另一个线程,在新的线程中也不会出现之前线程中设置的数据。
通过上面的例子就可以知道,ThreadLocal的作用就是提供了当前线程独立的值,这个值对其它线程是不可见的,其它线程也就不能使用和修改当前线程的值。
那么说回到Looper,通过前面的笔记我们也已经能够了解,Looper并不是只能存在于主线程中,在其它线程中我们也可以使用Looper来创建自己的消息循环机制。也就是说,Looper是和线程绑定的,主线程拥有主线程的Looper,子线程拥有子线程的Looper,主线程和子线程的Looper不能互相影响,所以我们看到在创建Looper的时候是通过ThreadLocal来保存Looper对象的,从而达到不同线程的Looper不会互相影响的作用。
相关变量
在ThreadLocal中定义了以下变量,根据注释可以明白变量的作用:
| 变量名 | 作用 |
| threadLocalHashCode | 这个变量通过final int修饰,通过静态方法nextHashCode()赋值。在类初始化的时候便会初始化这个参数的值,由于ThreadLocal的值其实是保存在一个Map结构的数据中,其中Map中的key便是ThreadLocal对象,value便是我们要保存的值。这个值可以认为是代表了初始化的那个对象,后面便是通过这个值进行Map的相关操作 |
| nextHashCode | 这是一个静态变量,生成下一个要使用的哈希码,原子更新,从0开始。nextHashCode()方法内部就是通过这个值加上一个固定的16进制的数字来生成下一个需要使用的threadLocalHashCode |
| HASH_INCREMENT | 这是一个常量,值为0x61c88647,表示每次生成哈希码的增量,nextHashCode()方法中使用nextHashCode值加上这个数字来生成下一个需要使用的threadLocalHashCode |
使用到的变量就是这些,下面是一些相关方法的学习。
set(T)
通过上面的例子我们知道,初始化一个ThreadLocal之后,我们会通过set()方法来进行赋值,下面是set(T)方法的源码:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
这个方法中的注释如下:将此线程局部变量的当前线程副本设置为指定值。大多数子类将不需要重写此方法,而仅依靠initialValue()方法来设置线程局部变量的值。
一般情况下,如果我们知道ThreadLocal中保存的值,那么我们可以通过重写initialValue()方法来指定值。但是有时候我们并不知道初始化的值,也可以通过这个方法来指定。
在这个方法中首先获取到当前的线程,然后通过getMap(t)方法获取到当前线程的ThreadLocalMap,下面是getMap(t)方法的源码:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可以看到是直接获取Thread中的threadLocals对象:
//Thread类中
ThreadLocal.ThreadLocalMap threadLocals = null;
判断如果获取到的ThreadLocalMap为空,则会执行createMap(t,value)来创建一个ThareadLocalMap:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
在这个方法里面创建了ThreadLocalMap对象,并把需要保存的值通过构造函数传递进去,下面是ThreadLocalMap的构造方法:
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);
}
在这里开始进入到了ThreadLoacalMap这个类,下面首先整体认识一下这个类。这个类的文档注释如下:
ThreadLocalMap是自定义的哈希映射,仅适用于维护线程局部值。没有操作导出到ThreadLocal类之外。该类是包私有的,以允许声明Thread类中的字段。为了帮助处理非常长的使用寿命,哈希表条目使用WeakReference作为键。但是,由于不使用参考队列,因此仅在表空间不足时,才保证删除过时的条目。
在这个类中,定义了一个Entry类来保存数据,关于这个类的注释如下:
此哈希映射中的条目使用其主引用字段作为键(始终是ThreadLocal对象),扩展了WeakReference.需要注意的是,空键(即entry.get() = null)意味着不再引用该键(从扩容方法中可以看出),因此可以从表中删除该条目,在下面的代码中,此类条目称为“陈旧条目”
相关属性如下:
| 属性名 | 作用 |
| INITIAL_CAPACITY | 这是一个常量,值为16,注释中指出这个值必须为2的幂 |
| table | 这是一个Entry数组,根据需要调整大小,length必须为2的幂 |
| size | 表中的条目数 |
| threshold | 下一个要调整大小的大小值,默认为0 |
了解了ThreadLoalMap的一些基本的信息,再来看创建ThreadLocalMap时的构造方法的源码:
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);
}
可以看到,在构造方法中做了如下操作:
- 创建
Entry数组,长度为16 - 获取传递过来的
ThreadLocal对象中的threadLocalHashCode的值,同时和15做与操作,得出当前的数据应该插入到什么位置。 - 创建一个新的
Entry类,保存ThreadLocal(key)和要保存的值firstValue(value),并插入到上一步计算的位置当中。 - 设置
size属性的值为1,表示当前数组中已经插入了一个值 - 调用
setThreshold(16)来确定下一次要增加的大小,这个方法源码如下:
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
可以看到,是对传入的参数做了一个*2/3的操作,然后赋值给threshold属性。
至此,我们第一次向ThreadLocalMap中添加数据的时候这个过程就结束了。整个流程相对还是比较简单的。
set(T) --ThreadLocalMap不为空
设置数据的时候,我们已经知道,当获取到当前线程的ThreadLocalMap为空的时候,会通过创建ThreadLocalMap对象来保存需要保存的数据。
查看源码,如果获取到的ThreadLocalMap不为空,此时会直接调用map.set(this,value)来设置数据,下面是这个方法的源码:
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
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();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
这个方法的源码注释如下:
我们不像get()那样使用快速路径,因为使用set()创建条目和替换现有条目至少是存在其中一种情况的,在这种情况下,快速路径失败的可能性会更高。
所以这里就有一个问题,get()是如何获取路径的,和set()有什么区别?这个问题先记录下来,查看get()源码的时候再去思考。
在上面的源码中,操作过程如下:
- 计算当要创建或者修改的条目所在的位置,计算方法仍然是之前的
ThreadLocal中的threadLocalHashCode值和数组的长度 - 1做与运算,这里需要注意的是,数组的初始容量是2的n次幂,同时规定了数组的容量也必须是2的n次幂(这个在扩容的时候每次扩容是当前数组长度的2倍就可以保证了). - 接下来进入到一个
for循环,获取上一步中计算的位置的Entry对象,然后判断是否为空,如果为空就直接跳出循环,将当前需要设置的数据直接创建Entry对象设置到数组中指定的位置上。如果不为空,则判断当前位置上这个Entry对象的key是否和要设置/修改的key一样,一样的话就直接修改Entry中value的值,不一样则通过nextIndex(i,len)来获取下一个位置上的Entry,一直循环知道结束或者找到满足条件的Entry。下面是nextIndex(i,len)方法的源码:
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
通过上面的源码可以看到,这里只是简单的判断了下一个数组下标是否越界,如果越界了就从下标0开始,没有越界则使用当前的下标。
这里之所以要这样做,主要就是因为哈希码可能存在冲突,为了解决冲突,这里使用的是线性探测法,也就是说我需要插入一个数据,但是发现要插入的位置已经有数据了(hash冲突),并且这个位置的数据和我要插入的数据的key并不一样,那我就找下一个位置去插入或者修改。结合之前的源码,其实我们能够知道,数组的初始长度为16,第一次赋值占用一个位置,那么后面每次设置值的时候总能找到空的位置,每次执行完向空位置插入数据的操作,都会去判断数组是否需要扩容,如果需要扩容就去扩大数组的大小,从而不会出现元素没有位置可以插入的问题。
- 如果获取到的
Entry不为空,那么判断获取到的Entry的key和当前指定的key是否一样,一样的话直接修改Entry中value字段的值并返回。 - 如果发现
key不一样,那么判断key是否为空,为空,则调用replaceStaleEntry(key,value,i)来替换当前Entry中key对应的value值并返回。下面是replaceStaleEntry(key,value,i)的源码:
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever
// occurs first
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we did not find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
在这个方法中,首先设置slotToExpunge变量的值为现在要替换的位置(提前说明一下,slotToExpunge保存的是需要清理的位置),然后从这个位置的前一个位置开始,向前查找是否还有Entry中key为null的对象,如果有就记录下来,由于这是一个环形的数组,因此循环会在遇到第一个Entry为空的时候停止,或者循环跑了一圈又回到了开始的位置停止。这样,slotToExpunge变量中保存的其实是另一个(或者同一个)Entry的key为null的下标。
接着开始下一个循环,首先考虑一种情况:刚开始我希望将一个数据保存在数组下表为10的这个Entry中,但是不巧由于哈希冲突数组下表为10的Entry不为空,那没有办法,就只能往后面找,找了一会,找到数组下表为13的位置是空的,没有数据,此时我便把要保存的数据保存在了数组下标为13的位置上。保存完之后,过了一段时间,数组下表为10的位置的Entry中的key被清理了,变为了null。这样等我下次想要对之前的保存的数据进行修改的时候,哈希计算出来的位置还是10,但是10上的Entry的key已经为空了,所以我就从10这个位置向后面找,看看能不能找到我想要修改的那个数据,最后在肯定会在13的位置上找到,找到之后,我就把数据修改了。然后呢我就把位置13上的数据和位置10上的数据交换位置,这样下次我在需要修改原先保存的数据的时候,我就可以通过哈希计算得到下标10,然后直接修改就好了,不再需要向后面遍历了。所以,这个时候我们要清理的就是下标为13的位置的数据了。第二个循环首先就是做了这个事情,对应如下源码:
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
接着上面的思路,slotToExpunge保存的是需要清理的位置下标,staleSlot是一开始传递进来的要清理的位置的下标,经过第一次的循环之后,slotToExpunge如果还是和staleSlot相等,那就说明需要清理的就是这个位置,但是由于经过第二次循环,staleSlot可能和i交换了位置,上面也说了,这种情况下需要清理的是位置i的数据,因此这里给slotToExpunge赋值为i,并执行了cleanSomeSlots(expungeStaleEntry(slotToExpunge), len)方法,首先是expungeStaleEntry(slotToExpunge)方法的源码如下:
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
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.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
从这个方法的注释来看,这个方法的作用是将当前要清理的位置上的数据进行清理,同时会检查当前位置到下一个不为空的位置上的hash值是否对应,如果不对应那么也会判断这个不对应的位置上的数据是否需要交换。
假设这样一个例子,有一个ThreadLocal需要保存,通过hash值计算出来的位置为2,那么就把这个ThreadLocal存储在数组下标为2的位置上,接着又有一个ThreadLocal被创建,计算出来的位置为3(其实这个情况发生概率比较小,这里仅做说明)。那么又把这个ThreadLocal保存在3的位置上,接下来又有一个ThreadLocal被创建,计算出来的位置也是2,出现了hash冲突,那么根据之前的规律,我们已经知道这个ThreadLocal将会保存在4这个位置上,然后又出现一个ThreadLocal,计算出来的位置是3,由于hash冲突,我们知道这个ThreadLocal将会被保存在5这个位置上。过了一段时间,3位置上面的ThreadLocal被清理了,导致位置3上的Entry的key变为了null,此时我们想要修改原来位置5上的数据,这个数据通过hash计算位置为3,但是由于此时这个位置的ThreadLocal被清理,导致key为空,根据之前的代码,会从这里开始遍历查找下一个key相同或者位置为空的位置,然后就找到了5,找到5之后,修改了数据,由于3位置key为空,则会把3位置上的Entry清理掉,然后把3和5的位置上的数据进行交换,这时3位置上有了数据,5位置上没了数据。做完这个,就开始从3位置遍历,一直遍历到下一个位置为空的地方,在这个例子中会遍历到位置为5的地方停止。之所以要做这个遍历,就是2和4位置上同样出现了hash冲突,此时2位置上的key也可能被清理了,那么就需要把4位置上的数据设置到2位置上。(需要注意的是,如果位置4上的key被清理了,那么就直接设置位置4上的数据为空,如果位置2上的key没被清理,那么则会从位置2开始往下寻找下一个位置,那么此时可能寻找的位置还是4,那这样数据就没有变化)。
个人理解这里为什么要做的这么复杂,可能是因为上面提到的两个例子本身发生的概率就比较小,计算hash的那个数在很多时候都可以完成完美的散列排布,所以每当发生hash冲突的时候,就把这些需要考虑的因素都考虑进去,因为下一次发生hash冲突还不知道在什么时候呢,避免了这些脏数据无法回收。另外一点我个人觉得也可能是因为get()方法我们在前面提到过,它在获取数据的时候就比set()方便,这里这样操作之后,会让get()方法少处理一些逻辑。但是这是我个人的猜测,不一定准确。
这个方法最终会返回我们修改的那个位置的下一个位置,如果没有数据被修改,默认返回我们在上一步交换的那个位置的下一个位置。
下面进入到cleanSomeSlots(int i, int n)方法的源码中:
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 = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
从这个方法的注释来看,这个方法使用启发式扫描某些单元来检查陈旧条目当添加了新元素或者删除另一个旧元素会调用这个方法。具体执行仍然是通过遍历来查看当前位置的Entry的key(ThreadLocal)是否为空,如果为空则仍然是调用我们刚才分析的expungeStaleEntry(i)来执行删除元素的操作。
需要注意的是,这里的循环条件使用到了无符号右移的操作,这里如果数组的长度为16,那么无符号右移的操作能够使得循环执行4次(这里是do...while...循环,一开始就会执行一次),15的二进制数为1111,无符号右移一位分别是0111,0011,0001,0000也就是15,7,3,1的是否会分别执行一次。
而所谓的启发式扫描,则是因为一旦发现有key为空的情况,则会重置n的值,导致循环的次数增加。而最坏的条件则是需要把整个数组都扫描一遍,所以注释中也说这个方法可能会导致某些时候set(T)数据时间复杂度为O(n)。注意这是我个人的理解,我没有找到启发式扫面的具体含义,根据源码的执行流程产生的这样的理解,也不知道是否正确。
上面所说的都是我们期望能够在key == null的这个位置的后面找到我们要插入/修改数据的那个key,但是其实很多时候我们并不能知道,原因也是因为很少会出现hash冲突,从上面的执行流程来看,多次用到了遍历,这也会导致时间复杂度较高,所以上面的方法应该尽可能少的被使用到。
那么接下来就是如果没有找到我们要设置/修改的那个key,我们就把当前位置的Entry移除,然后把现在新的数据设置上去,对应源码中的如下操作:
// If key not found, put new entry in stale slot
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
replaceStaleEntry方法一开始就会从当前这个key为空的位置开始向前查找这个位置之前还有没有key为空的位置,如果找到了这个位置,那么就从这个位置开始清理key为空的数据,对应源码中的如下操作:
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
我们需要注意的是,数组会根据情况进行扩容的操作,所以不会存在数据中的数据被存满的情况,所以说之前的遍历肯定会遇到某一个位置数据为空然后停下来,不会存在死循环的情况。
至此,当我们要设置/修改的位置上的Entry的key为空的情况就判断完了。
- 程序执行到此处,说明当前数组中没有找到要设置/修改的
Entry,那么就创建一个新的Entry保存key和value, - 最后,如果我们没有清理数据,并且当前已经存在的数据大于之前我们设置的阈值,那么就进行
rehash()的操作。
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
可以看到,这里首先进行了expungeStaleEntries()方法,源码如下:
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
在这个方法中通过遍历整个数组来清理那些key == null的元素。
清理完数据之后,判断当前已经存在的数据是否大于等于threshold - threshold / 4这个阈值,如果大于等于这个值,则调用resize()方法进行扩容,源码如下:
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
可以看到,扩容的方法其实很简单,主要做了如下操作:
- 设置新数组的长度为原来数组长度的2倍,由于一开始的数组长度为16,所以每次扩容都是可以保证数组的长度是2的n次幂的。
- 遍历原来的数组,判断每个位置的
Entry的key(ThreadLocal)是否为空,如果为空,则设置其中的value也为空来帮助GC. - 如果不为空,则通过
threadLocalHashCode计算位置,如果位置有冲突,则循环计算下一个可以插入的位置,之后将数据保存进去。 - 设置扩容的阈值,当前已经填充的数据量,数组变量指向新的数组。扩容完成。
至此,set(T)方法就分析完了,这个方法分析完成后,可以发现其实里面的大部分方法都分析完了。
T get()
get()方法的源码如下:
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();
}
可以看到,仍然是首先获取当前线程,然后获取到当前线程的ThreadLocalMap,如果获取到的ThreadLocalMap不为空,则调用map.getEntry(this)获取数据最后返回,其它情况下调用setInitialValue()方法。
map.getEntry(this)
首先看map.getEntry(this)方法的源码:
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);
}
逻辑比较简单,首先仍然是通过threadLocalHashCode值计算位置,计算出来之后,如果这个位置上的ThreadLocalMap.Entry不为空并且key也相同,那就直接返回这个Entry。否则调用getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)方法,源码如下:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
逻辑也很简单,就是通过循环判断数组中的Entry的key有没有和需要的一样的,如果有就拿出来返回,没有就返回null。另外在遍历的时候如果发现有key == null的情况,仍然会调用expungeStaleEntry(i)方法进行清理。
setInitialValue()
如果获取到的ThreadLocalMap为空或者ThreadLocalMap中没有保存当前ThreadLocal对应的值,那么会调用这个方法,源码如下:
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()获取初始值,然后就和set(T)方法执行的逻辑一样了。不再赘述。
remove()方法
remove()方法的执行逻辑也比较简单,仍然是获取ThreadLocalMap,然后调用它里面的remove(ThreadLocal<?> t)方法删除数据,源码如下:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
可以看到,仍然是通过threadLocalHashCode计算位置,然后遍历key是否相等,如果相等则调用clear()清理对象,之后调用expungeStaleEntry(i)来清理位置。
需要注意的是,ThreadLocalMap.Entry是继承自WeakReference,上面的clear()方法内部会调用本地方法clearReferent()方法来清理引用,这个方法的注释如下: 禁止直接访问参照对象,并且在安全的情况下清理对象块并且将参照对象设置为null。
至此,关于ThreadLocal的源码就学习完了。