Android知识体系之ThreadLocal-------------Handler的灵魂

博客为 有时个哥 原创,如需转载请标明出处:https://mp.csdn.net/mdeditor/83142537
说明: 参考源码为Android7.0,请读者注意

如果大家仔细看过Handler 的源码之后,就会发现其主要角色Looper类中有个ThreadLocal对象。

  // sThreadLocal.get() will return null unless you've called prepare().
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

其实Handler 能实现线程之间通信主要的是因为ThreadLocal的功劳。

ThreadLocal是做什么的

ThreadLocal 是为线程提供的局部变量的类。它为线程提供一个独立的初始化副本变量。

我们先来大体看一下ThreadLocal 的工作原理结构。ThreadLocal 原理结构图

下面我们通过分析代码一起来看一下ThreadLocal的原理。

ThreadLocalMap

首先看一下ThreadLocalMap。
ThreadLocalMap是ThreadLocal的静态内部类。


    /**
     * ThreadLocalMap is a customized hash map suitable only for
     * maintaining thread local values. No operations are exported
     * outside of the ThreadLocal class. The class is package private to
     * allow declaration of fields in class Thread.  To help deal with
     * very large and long-lived usages, the hash table entries use
     * WeakReferences for keys. However, since reference queues are not
     * used, stale entries are guaranteed to be removed only when
     * the table starts running out of space.
     */
    static class ThreadLocalMap {

通过ThreadLocal 的类的介绍说明我们知道这么几点:

  • ThreadLocalMap 是一个定制的Map, 只适合维护线程本地值。
  • 除了ThreaLocal类外,其他类不对其进行操作。
  • 为了处理大的和周期长的变量,内部的Hash表的Key采用了弱引用。
  • 只有在耗尽空间时才会主动删除陈旧无用的条目。
    ThreadLocalMap中也有一个静态内部类Entry
 /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

通过代码我们可以知道几点:

  • Entry 集成了 WeakReference<ThreadLocal<?>>,所以其Key值为弱引用(这一点要注意)。
  • Entry 可以接收的key永远是ThreadLocal 。
  • Entry 对象是真正存储数据的单元。
  • 当key为null 时,此条目就可以从表中移除

Key为什么设计成弱引用

Map类的介绍中提到了,是针对大的周期长的数据对象。 因为线程一般执行的任务的时间比较长, 有很多要存储的变量对象的周期可能存在于整个线程任务周期中, 数据量比较大的对象也是很常见的,但是当你存储的时候使用的是强引用, 则当线程执行完时,这些变量对象不会立马回收掉(具体的可以去了解一下GC的回收机制)。这样就造成了内存没有及时的被释放回收,从而可能造成其他的问题(比如由于内存占用达到极值造成oom异常等)。如果当对象没有强引用指向时,只有弱引执向时,gc 执行时则会直接回收此对象。所以设计出弱引用,能更好的及时的回收对象,释放内存空间。

但是要注意的是,虽然Entry 类的key 是弱引用,但是Entry是强引用,当key被释放可,则Entry的key就为null,变成了无用数据, 但是由于Entry强引用,所以要记得要调用Map 的remove方法把其移除,否则就造成了内存泄漏。

ThreaLocal类外,其他类不对ThreaLocalMap进行操作

通过一系列的设计,也会看到一直遵循着这个设计理念,比如ThreaLocalMap 是ThreadLocal内部类,并且Map的方法都是私有的,这就导致了Map的接口调用只能在ThreadLocal内部;Map只接受ThreadLocal 类型的key(实际上真正的是Entry),ThreadLocal 中的代码编写也有体现(下面会逐步的讲到ThreadLocal代码). 遵循这一设计理念,是为了保证只有ThreadLocal 一个入口操作,保证数据操作安全。

定制的Map, 只适合维护线程本地值

假如你看过Thread 的代码,你会发现在Thread 会持有一个ThreadLocal.ThreadLocalMap 的变量threadLocals。就是这个变量来进行维护数据的。

ThreaddLocalMap的实现
Map的内部是有一个Entry[] 维护着数据, 大小默认是16;

 /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

 /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

通过hash值得计算来得到Entry[] 数组的下标,即数据的存储位置。

 /**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        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);
        }

setThreshold()方法是设置调制Map大小的阈值。

/**
         * Set the resize threshold to maintain at worst a 2/3 load factor.
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

threshold 就是调整Map大小的阈值了。

然后又几个主要方法: set getEntry remove等

Set

 /**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be set
         */
        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();
        }

set方法中首先用HashCode计算出了数组的下标,当对应下标的位置有数据存在时,则进入for循环。 for 循环做了两个事(检查两种特殊的情况),1 是判断当前循环取得的数据是否和要存储的数据一样(包括键值对); 2 当前循环获得的数据是否key为null,为null则用当前的新值替换上。无论满足哪种情况,都会直接return出方法。否则就按照新的方法安排新值。

get 方法

 /**
         * Get the entry associated with key.  This method
         * itself handles only the fast path: a direct hit of existing
         * key. It otherwise relays to getEntryAfterMiss.  This is
         * designed to maximize performance for direct hits, in part
         * by making this method readily inlinable.
         *
         * @param  key the thread local object
         * @return the entry associated with key, or null if no such
         */
        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);
        }

get 方法也是先计算下标,然后查找数据, 如果数据不为空,并且key相等,则直接返回,否则就按规则计算出下标,找到存放的位置。具体的规则实现有兴趣的可以查看源码。

由代码可以看出ThreadLocalMap和HashMap的结构还是有不一样的地方; HashMap是数组加链表实现的,而ThreadLocalMap 而完全是用数组存储。

ThreadLocal

ThreadLocal 其实就是对ThreadLocalMap的操作封装。由于ThreadLocalMap 的方法基本是私有的,所以除ThreadLocal 以外的类,无法对ThreadLocalMap进行直接操作。

ThreadLocal 也有set, get 方法。 这两个也是其主要的方法。先看set方法。

set

 /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

首先看set是没有key的参数,只有value。所以外部无法决定其value是谁的数据变量, 只是负责把value传入进来。然后得到当前线程, 接着调用了getMap()。

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

getMap() 的代码非常简单,只是把当前线程的threadLocals返回,这是一个ThreadLocalMap类型。就是这个类型真正的存储维护线程的数据变量。
首先查看线程的 threadLocals 是否为空,不为空则直接把数据存储进去了。
若为空,则调用createMap() 创建ThreadLocalMap();

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

通过代码可以看出,new 了一个ThreadLocalMap对象,赋值给了线程的threadLocals字段。

虽然代码不多,但是需要我们好好深思理解。

  • 外部无法传入key, 所以存储是有内部决定。 通过Thread.currentThread()得到当前线程,从而与当前线程做了操作绑定。这就是为什么ThreadLocal 在多线程的情况下还能做到数据存储不错乱。因为set的数据只能和当前的线程有关系。
  • Thread 的map为null时,ThreadLocal要为其初始化Map。
  • Thread map存在时, 则直接对把数据添加到Map中。这句代码意味着,真正维持数据的是线程自己。 ThreadLocal只是其他一个获取Map操作的入口。

我们结合一下上面的工作原理图, 你会发现Thread1中的Map即有ThreadLocal1 key的数据,又有ThreadLocal2 作为key的数据, Thread2也是一样。这就是说明上面的第三点。 ThreadLocal是入口,只负责对Thread的Map的数据操作。只要是在当前Thread中调用的ThreadLocal 的set方法都是操作Map的入口, 无论他是ThreadLocal1还是ThreadLocal2.
结合一下代码分析可能更容易明白:

 if (map != null)
            map.set(this, value);
        else
            createMap(t, value);

看set方法中的这段代码,假如map不为null, 是ThreadLocal1 给线程初始化的。当前是ThreadLocal2. 按照代码逻辑,则会直接存储数据。所以Thread 中的Map的key既有ThreadLocal1又有ThreadLocal2.

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

get方法也很简单,也是没有传入key。保证了只能取得当前线程的Map。

总结
ThreadLocal 就是一个仅能够对当前线程变量副本(ThreadLocalMap)操作的类。由于其set,get的设计,能做到各个线程存储数据操作的隔离。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值