Android Handler学习笔记(三)

回顾

 经过前面两篇笔记的学习,已经知道了Handler如何使用,Looper的建立和做用,MessageQueue的建立和做用,Message的建立和做用。数组

  1. Handler在主线程中建立,在子线程中发送消息,经过调用sendMessage()post()系列重载方法来发送消息。发送消息的时候会将须要处理消息的时间一并携带上去,而后根据时间将Message添加到MessageQueue中相应的位置。安全

  2. Looper在主线程建立的时候就会经过调用prepareMainLooper()方法建立,将当前线程标记为一个不可退出的循环线程。同时会建立MessageQueue对象,而后启动loop()方法不断地从MessageQueue中取出须要操做的Message对象,经过它的target属性指定的Handler,调用Handler中对用的方法去处理消息。bash

  3. MessageQueue是一个根据时间建立的消息队列,在Looper中的构造方法执行的时候同时会建立出MessageQueue对象。主要负责消息的入队和出队的操做。app

  4. Message是消息的实体,表示具体要执行的内容,Message内部经过维护一个对象池来实现Message对象的复用。在从obtain()方法中获取到插入到MessageQueue的这段时间处于没有使用的状态,在插入到MessageQueuerecycleUnchecked()方法回收处于使用中的状态。ide

 这篇笔记主要学习在Looper中拿到Message以后具体是如何处理的。函数

Handler.dispatchMessage(Message)方法源码

 在第一篇笔记中,咱们已经了解了,在Looperloop()方法中执行循环获取下一个Message以后,会经过msg.target.dispatchMessage(msg)来分发当前Message给对应的Handler,下面是这个方法的源码:oop

/**
 * 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);
    }
}
复制代码
  1. 在这个方法中,首先会判断当前Messagecallback是否为空,经过上一篇笔记能够了解到,Messagecallback的类型是Runnable,经过Handler.post(Runnable)系列构造函数发送消息的时候会给Messagecallback属性赋值,源码以下:
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;
}
复制代码
  1. 若是Messagecallback属性不为空,则会执行handCallback(Message)方法,源码以下:
private static void handleCallback(Message message) {
    message.callback.run();
}
复制代码

 源码中能够看到,这里也只是执行的Messagecallback属性所对应的Runnablerun()方法。post

  1. 若是Messagecallback属性为空,也就是说不是经过post()系列重载方法发送的消息,那么接下里就判断Handler中的mCallback属性是否为空。

mCallbackHandler.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,避免了可能出现的内存泄漏的问题。ui

  1. 若是mCallback属性的值不为空,那么就经过这个接口来处理Message,同时查看也能知道,须要注意若是成功处理了Message,那么就要返回true,这个方法默认实现返回false。由于若是处理成功了还返回false,则会继续执行下面的handleMessage(msg)方法,这是没有必要的。

  2. 若是mCallback属性的值为空,那么就调用handleMessage(Message)方法来处理Message,这个方法通常须要咱们重写它的实现,源码中默认是空的实现:

/**
 * Subclasses must implement this to receive messages.
 */
public void handleMessage(Message msg) {
}
复制代码

 至此,Handler的整个处理流程就结束了,以下图所示:

Handler总体执行流程
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类提供了线程局部变量,这些变量与普通变量不一样,每一个线程均可以经过其getset方法来访问本身独立的初始化的变量副本。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);
}
复制代码

 能够看到,在构造方法中作了以下操做:

  1. 建立Entry数组,长度为16
  2. 获取传递过来的ThreadLocal对象中的threadLocalHashCode的值,同时和15作与操做,得出当前的数据应该插入到什么位置。
  3. 建立一个新的Entry类,保存ThreadLocal(key)和要保存的值firstValue(value),并插入到上一步计算的位置当中。
  4. 设置size属性的值为1,表示当前数组中已经插入了一个值
  5. 调用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()源码的时候再去思考。

 在上面的源码中,操做过程以下:

  1. 计算当要建立或者修改的条目所在的位置,计算方法仍然是以前的ThreadLocal中的threadLocalHashCode值和数组的长度 - 1作与运算,这里须要注意的是,数组的初始容量是2的n次幂,同时规定了数组的容量也必须是2的n次幂(这个在扩容的时候每次扩容是当前数组长度的2倍就能够保证了).
  2. 接下来进入到一个for循环,获取上一步中计算的位置的Entry对象,而后判断是否为空,若是为空就直接跳出循环,将当前须要设置的数据直接建立Entry对象设置到数组中指定的位置上。若是不为空,则判断当前位置上这个Entry对象的key是否和要设置/修改的key同样,同样的话就直接修改Entryvalue的值,不同则经过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,第一次赋值占用一个位置,那么后面每次设置值的时候总能找到空的位置,每次执行完向空位置插入数据的操做,都会去判断数组是否须要扩容,若是须要扩容就去扩大数组的大小,从而不会出现元素没有位置能够插入的问题。

  1. 若是获取到的Entry不为空,那么判断获取到的Entrykey和当前指定的key是否同样,同样的话直接修改Entryvalue字段的值并返回。
  2. 若是发现key不同,那么判断key是否为空,为空,则调用replaceStaleEntry(key,value,i)来替换当前Entrykey对应的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保存的是须要清理的位置),而后从这个位置的前一个位置开始,向前查找是否还有Entrykeynull的对象,若是有就记录下来,因为这是一个环形的数组,所以循环会在遇到第一个Entry为空的时候中止,或者循环跑了一圈又回到了开始的位置中止。这样,slotToExpunge变量中保存的实际上是另外一个(或者同一个)Entrykeynull的下标。

 接着开始下一个循环,首先考虑一种状况:刚开始我但愿将一个数据保存在数组下表为10的这个Entry中,可是不巧因为哈希冲突数组下表为10的Entry不为空,那没有办法,就只能日后面找,找了一会,找到数组下表为13的位置是空的,没有数据,此时我便把要保存的数据保存在了数组下标为13的位置上。保存完以后,过了一段时间,数组下表为10的位置的Entry中的key被清理了,变为了null。这样等我下次想要对以前的保存的数据进行修改的时候,哈希计算出来的位置仍是10,可是10上的Entrykey已经为空了,因此我就从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上的Entrykey变为了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;
}
复制代码

 从这个方法的注释来看,这个方法使用启发式扫描某些单元来检查陈旧条目当添加了新元素或者删除另外一个旧元素会调用这个方法。具体执行仍然是经过遍从来查看当前位置的Entrykey(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);
复制代码

咱们须要注意的是,数组会根据状况进行扩容的操做,因此不会存在数据中的数据被存满的状况,因此说以前的遍历确定会遇到某一个位置数据为空而后停下来,不会存在死循环的状况。

 至此,当咱们要设置/修改的位置上的Entrykey为空的状况就判断完了。

  1. 程序执行到此处,说明当前数组中没有找到要设置/修改的Entry,那么就建立一个新的Entry保存keyvalue,
  2. 最后,若是咱们没有清理数据,而且当前已经存在的数据大于以前咱们设置的阈值,那么就进行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;
}
复制代码

 能够看到,扩容的方法其实很简单,主要作了以下操做:

  1. 设置新数组的长度为原来数组长度的2倍,因为一开始的数组长度为16,因此每次扩容都是能够保证数组的长度是2的n次幂的。
  2. 遍历原来的数组,判断每一个位置的Entrykey(ThreadLocal)是否为空,若是为空,则设置其中的value也为空来帮助GC.
  3. 若是不为空,则经过threadLocalHashCode计算位置,若是位置有冲突,则循环计算下一个能够插入的位置,以后将数据保存进去。
  4. 设置扩容的阈值,当前已经填充的数据量,数组变量指向新的数组。扩容完成。

 至此,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;
}
复制代码

 逻辑也很简单,就是经过循环判断数组中的Entrykey有没有和须要的同样的,若是有就拿出来返回,没有就返回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的源码就学习完了。

相关文章
相关标签/搜索