对ThreadLocal在Handler中的应用的一些理解

前言
JDK源码的ThreadLocal类和Android SDK的ThreadLocal类细节略有不同,但原理和实现的功能是相同的。本文的代码均来自Android SDK源码。
看下Android SDK源码里ThreadLocal的注释:

/**
 * Implements a thread-local storage, that is, a variable for which each thread
 * has its own value. All threads share the same {@code ThreadLocal} object,
 * but each sees a different value when accessing it, and changes made by one
 * thread do not affect the other threads. The implementation supports
 * {@code null} values.
 *
 * @see java.lang.Thread
 * @author Bob Lee
 */

就不翻译了。我的理解,ThreadLocal是“线程内部存储”,它不是一个线程,而是用于存储对象,线程内部获取到的这个对象是唯一的,而不同线程获取的这个对象则是不同的对象。

也即,这个对象的作用域是线程,而不是平常我们使用的包内,类内或者方法内等。

说个题外话,源码注释还有这么一句:

/* Thanks to Josh Bloch and Doug Lea for code reviews and impl advice. */

作者特意在源码注释里表示了对Josh Bloch和Doug Lea的感谢,谢谢他们的review和建议。原来这部分代码还有这两位Java大牛的参与~不多说,下面正式开始分析。

Handler与ThreadLocal
在Android中,一个典型的ThreadLocal的使用场景就是Handler类的实现源码里,ThreadLocal用于存储当前线程的Looper对象,从而把线程和Looper对象实现一一对应的关系。

下面具体看下源码实现:
Looper类里,有个静态变量sThreadLocal,它存储的对象是Looper对象:

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

而我们都知道,如果想在一个新线程里自定义Handler并使用的话,就必须先调用Looper.prepare(),然后定义Handler之后,再调用Looper.loop()。

Looper.prepare()里有这么一行代码:

sThreadLocal.set(new Looper(quitAllowed));

这一行的作用,就是把当前线程与新创建的Looper对象对应起来,那具体是怎么实现对应的呢?

如何保证Looper对象在线程里是唯一的
Looper的唯一的构造方法是在prepare()里调用的,当创建了一个Looper对象之后,立刻会被ThreadLocal的set()方法作为参数传入处理。

来看看ThreadLocal的set方法做了什么:

public void set(T value) {
    Thread currentThread = Thread.currentThread();//得到当前线程
    Values values = values(currentThread);// 得到当前线程的localValues变量的值
    if (values == null) {//首次执行时,会进入这里
        values = initializeValues(currentThread);//新创建一个Values对象
    }
    values.put(this, value);
}

1.来看下values()方法;

    Values values(Thread current) {
        return current.localValues;
    }

values()方法就是为了得到当前线程的localValues变量的值。那么这个localValues的值是什么?又是时候被赋值的呢?
看它在Thread类里的定义;

    ThreadLocal.Values localValues;

localValues它是个ThreadLocal的内部类Values类型的一个变量,权限是包内可访问,同时也没有对它的get和set方法。那么ThreadLocal的set方法首次获取当前线程的localValues就为null, 也就会走到initializeValues()方法里。

2.看下这个initializeValues()方法:

    Values initializeValues(Thread current) {
        return current.localValues = new Values();
    }

当前线程的localValues对象就是在这里创建的。

3.获取values对象后,最后是values.put(this, value),这个this就是sThreadLocal对象,value就是新创建的Looper对象。

这个put()方法就不分析了。可以把Values类当成一个Map(实际上Java的实现就是如此,而Android SDK里用的是一个Object数组,下标偶数的作为key,奇数下标的作为value,从而实现key 和value的一一对应。这部分可以理解为,每个线程有一个map, 这个map存储的每个键值对, Threadlocal对象为键, Looper对象为值。而这个线程里如果定义了其他ThreadLocal对象,也存在这个map里
,具体可以看下ThreadLocal的Values内部类的实现)。

也就是说, 对ThreadLocal对象的处理,实际上就是对当前线程的localValues对象里的Object数组的处理,因此操作就局限在了线程内部,也就实现了ThreadLocal可以在不同的线程存储不同的对象。


上面我们讲到,Looper的唯一的构造方法是在prepare()里调用的,当创建了一个Looper对象之后,立刻会被ThreadLocal的set()方法作为参数传入处理。

那么,线程会不会存在多个Looper对象,即Looper的构造方法会被多次调用呢?答案是不会。如果需要多个Looper对象,那就得多次调用Looper.prepare()。

    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的prepare()的源码,首次调用Looper.prepare()时,sThreadLocal键值对已经生成。当再次进入这个prepare()方法时,先判断sThreadLocal.get() 的值,看下get()的实现:

    public T get() {
        // Optimized for the fast path.
        Thread currentThread = Thread.currentThread();
        Values values = values(currentThread);
        if (values != null) {
            Object[] table = values.table;
            int index = hash & values.mask;
            if (this.reference == table[index]) {
                return (T) table[index + 1];
            }
        } else {
            values = initializeValues(currentThread);
        }

        return (T) values.getAfterMiss(this);
    }

ThreadLocal的 get() 和 set()可以对照查看。都是先获取当前线程,然后获取线程values的值,对values进行处理。在走到get()方法里的时候,如果之前调用过set(),那么values()已经不为null了,因此会返回sThreadLocal作为键,所存储的Looper对象。

sThreadLocal.get()不为null,那么Looper.prepare()就会抛出异常了。也就是重复调用Looper.prepare()会抛出异常,而不会创建多个Looper对象。

既然一个线程有只一个Looper对象,而默认会有一个存储此Looper对象的ThreadLocal。那么有个疑问:存储Looper对象时,所用的键值对的形式,为什么不用当前线程作为键呢?我的理解是,一个线程可能有多个ThreadLocal对象(这些ThreadLocal对象我们可以自己创建,并不一定存储Looper对象),因此,线程和ThreadLocal是一对多的关系,如果把线程作为键的话,无法存储不同的对象。

获取线程内唯一的Looper对象
上面的分析中,我们可以知道,一个线程里,sThreadLocal存储的Looper对象是唯一的,怎么获取这个线程唯一的Looper对象呢?
Looper类提供了一个myLooper()方法,用于获取Looper对象:

    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }

这也是获取looper对象唯一的方法:从sThreadLocal进行获取,因此也保证了获取到的Looper对象就是sThreadLocal所存储的对象,是此线程唯一的对象。

发布了91 篇原创文章 · 获赞 71 · 访问量 26万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览