ThreadLocal源码分析

写在前面

本次使用的源码来自于Android API 25 Platform.

引子

在Android中,在子线程中创建Handle人的时候,要求我们先调用Looper#prepare方法,来创建Looper,并且,一个线程中只有一个Looper。这是怎么实现的呢?

    static final ThreadLocal sThreadLocal = new ThreadLocal();
    public static void prepare() {
        prepare(true);
    }
    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));
    }

我们看源码可以发现,Looper中使用ThreadLocal来保存每个线程中新创建的Looper,并且每次创建前先检查ThreadLocal中是否存在了Looper,以此来确保线程中Looper的唯一性。
那么,这里就有一个问题了,ThreadLocal能保证线程中的Looper是唯一,它是怎么做到这一点的呢?

ThreadLocal源码分析

在上面的Looper中,我们看到有使用ThreadLocal#set和ThreadLocal#get,所以,我们先从这两个方法开始吧。

ThreadLocal#set

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

在这个方法中我们发现,数据实际上保存在ThreadLocalMap中, 如果先通过ThreadLocal#getMap来获取ThreadLocalMap,如果有ThreadLocalMap,则直接存储,没有则调用ThreadLocal#createMap。
在这里,我们先不去看什么是ThreadLocalMap, 只要记住ThreadLocal保存的数据被传进了ThreadLocalMap就行了,具体ThreadLocalMap是什么情况我们一会再看。现在我们先来看看ThreadLocal#getMap和ThreadLocal#createMap

ThreadLocal#getMap

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

首先,参数Thread是在ThreadLocal#set中我们通过Thread#currentThread获取的当前线程,该方法很简单,直接返回了当前线程的成员变量threadLocals, 那我们就进Thread去看看Thread#threadLocals是什么情况

Thread#threadLocals

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

这就更简单了,就是成员变量,没什么东西,那我们继续往下看。

ThreadLocal#createMap

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

ThreadLocal#createMap 就是给当前线程的成员变量Thread#threadLocals赋值。
现在我们应该已经有点眉目了,刚开始,直观上,觉得数据是保存在ThreadLocal中的,到这里我们可以判断,数据实际上是保存在当前线程中的,通过Thread#threadLocals持有数据。
我们继续往下看,看看这种判断是否正确。

ThreadLocal#get

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }

通过上面的取数据,我们大致印证了刚才的判断,就是数据实际上被存储到当前线程中,因为取数据正是从正式我们当前线程的成员变量Thread#threadLocals中取。
这里,我们已经可以解释刚开始的那个问题了,为什么ThreadLocal能保证线程中Looper的唯一性,那是因为通过ThreadLocal,数据实际上被保存在每个线程中,每个线程的Thread#threadLocals是独立互不干扰的,所以他们保存在Thread#threadLocals中的数据也算是独立互不干扰的,也就可以保证Looper的唯一性。

下面,我们继续研究ThreadLocal.ThreadLocalMap。研究ThreadLocalMap我们 也从上面曾经出现过的ThreadLocalMap的三个方法1.ThreadLocalMap#set 2.ThreadLocalMap#getEntry 3.构造方法 开始研究。
在此之前,我们先了解一下ThreadLocal.ThreadLocalMap.Entry

ThreadLocal.ThreadLocalMap.Entry

        static class Entry extends WeakReference {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }

代码很简单,就是继承自WeakReference的数据类,类中有两个参数,一个是ThreadLocal,另外一个正是我们要保存的数据,类型是Object。

ThreadLocal.ThreadLocalMap#set

        private Entry[] table;
        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();
              //如果数组中i位置的元素的变量ThreadLocal与要设置的数据的ThreadLocal一样,
              //则直接将i位置元素的值value替换为设置的数据
                if (k == key) {
                    e.value = value;
                    return;
                }
               //如果数组中i位置的元素的变量ThreadLocal已经是null(为什么会出现null,
               //因为Entry 继承自WeakReference,以弱引用的方式持有ThreadLocal),
               //那么,直接替换位置i的元素,设置进来的数据将被保存在这个位置
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
           //最后,这是正常的情况了,数据被保存在数组中首次没有元素值得位置,比如数组长度20,
           //只有前10个位置存有数据,那么,新来的数据就被放到了数组中角标为10的位置。
            tab[i] = new Entry(key, value);
            int sz = ++size;
            //检查数组长度是否超过阀值,超过的话,对数组进行扩容
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

ThreadLocalMap通过一个数组来保存数据。

ThreadLocalMap#getEntry

        private Entry getEntry(ThreadLocal key) {
            //获取数据在数组中的位置
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            //获取i位置的元素,如果元素中的ThreadLocal不为空且和要获取数据的ThreadLocal一样
            if (e != null && e.get() == key) 
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

ThreadLocalMap#getEntryAfterMiss

        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;
        }

这里,遍历数组,如果数组中存在某个元素的ThreadLocal跟key一样且不是null,就返回这个元素,如果元素中的ThreadLocal为null,则调用ThreadLocalMap#expungeStaleEntry,将这个位置的元素置为null。
感觉跑偏了啊,不要在意这些细节,我们再总结一下,ThreadLocalMap里面通过一个数据来存放数据,就这么简单。
下面我们继续看ThreadLocalMap的构造函数

ThreadLocal.ThreadLocalMap#ThreadLocalMap(java.lang.ThreadLocal, java.lang.Object)

 private static final int INITIAL_CAPACITY = 16;

        ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
            //初始化保存数据的数组,且数组初始长度16
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            //保存第一个元素到数组中
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

构造函数中做的事情很简单,就是初始化了长度为16的数组,然后保存第一个元素。

另外,这个数组并不是定长的,我们可以在ThreadLocal.ThreadLocalMap#rehash中看到它的扩容逻辑

ThreadLocal.ThreadLocalMap#rehash

        private void rehash() {
            //先遍历数组,移除掉数组中为null的元素和不为null但是元素的变量ThreadLocal为null的元素
            expungeStaleEntries();

            // Use lower threshold for doubling to avoid hysteresis
            if (size >= threshold - threshold / 4)
                resize();
           //threshold 为数组长度的2/3,这里,如果新添加元素后,长度不小于数组长度的一半,就进行扩容
        }

        /**
         * Double the capacity of the table.
         */
        private void resize() {
            //扩容的逻辑很简单,直接将原数组的长度增加一倍
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            ...
            table = newTab;
        }
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

ThreadLocal.ThreadLocalMap#rehash正是在ThreadLocal.ThreadLocalMap#set的最后调用的。每次添加新元素后,检查添加后数组长度是否大于阀值,如果大于阀值,直接将原数组长度增加一倍。

总结

  1. ThreadLocal并不保存数据,数据实际保存在当前线程中(java.lang.Thread#threadLocals)
  2. Thread#threadLocals中实际是通过数组来保存数据

你可能感兴趣的:(ThreadLocal源码分析)