Android消息机制之ThreadLocal浅析

概述

首先,需要注意的是,ThreadLocal不是 Thread,它是一个 Thread 内部的数据存储类,通过它可以在指定的线程中存储数据,将数据存储后,只有在指定线程中才可以获取到存储的数据,对于其他线程来说则是无法获取到该数据的。 从而达到 线程间数据隔离 的目的。

下面是ThreadLocal 在 Thread 中的定义:在这里插入图片描述
可见,ThreadLocal 在线程中是以ThreadLocalMap形式存在的,而后面我们会知道,真正保存数据的地方就是在ThreadLocalMap

Android 应用开发中能用到 ThreadLocal的场景比较少,但在系统的 Looper、 ActivityThread、 AMS 、编舞者Choreographer 等源码都用到了ThreadLocal。一般来说,当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用 ThreadLocal。比如,对于消息机制的 Handler 来说,它需要获取当前线程的 Looper,而 Looper 的作用域就是线程并且不同线程具有不同的 Looper,这时候通过 ThreadLocal 就可以轻松实现 Looper 在线程中的存取。因此,一个线程只有一个 Looper 的原因,就是 ThreadLocal 在起作用。

举例

上面的介绍可能会比较抽象,下面通过一个简单的例子,来说明 ThreadLocal的基本使用。

        final ThreadLocal<String> threadLocal = new ThreadLocal<>();
        new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocal.set("Thread 1");
                Log.e("twj", Thread.currentThread().getName() + "  " + threadLocal.get());
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocal.set("Thread 2");
                Log.e("twj", Thread.currentThread().getName() + "  " + threadLocal.get());
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                Log.e("twj", Thread.currentThread().getName() + "  " + threadLocal.get());
            }
        }).start();

创建三个线程,第 1 和第 2 个 线程都通过ThreadLocal设置了值,第 3 个线程没有设置,看下结果:

2019-02-21 15:28:27.216 13845-14079/com.hust_twj.zademo E/twj: Thread-30  Thread 1
2019-02-21 15:28:27.216 13845-14080/com.hust_twj.zademo E/twj: Thread-31  Thread 2
2019-02-21 15:28:27.217 13845-14081/com.hust_twj.zademo E/twj: Thread-32  null

可以看到,第 1 和第 2 个 线程成功获取到了所设置的 String 类型值,第 3 个线程由于没有设置值,因此获取到的数据为 null。也就说明,即便所有线程操作的是同一个 ThreadLocal对象,但对于 ThreadLocal中存储的数据,在不同的线程中具有不同的数据副本,只有存储后才能获取到相应的数据,否则就获取不到。

原理

ThreadLocal为泛型类:public class ThreadLocal<T>,传进来的参数 T 即为要保存的数据的类型。那么数据是存储在哪里呢?答案是ThreadLocalMapThreadLocalMapThreadLocal 存储数据的内部类。我们来看下其源码:

ThreadLocalMap
 static class ThreadLocalMap {

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

            Entry(ThreadLocal k, Object v) {
                // 注意这里的 key 就是 ThreadLocal<?> k,实际上 Entry 对它仅持有一个弱引用
                super(k);
                value = v;
            }
        }

        /**
         * 初始容量为16
         */
        private static final int INITIAL_CAPACITY = 16;

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

        /**
         * The number of entries in the table.
         */
        private int size = 0;

        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0
   
        // ...
        
        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);
       }

       /**
         * 阈值大小为数组容量 * 2 / 3.
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }
}

可以看出,ThreadLocalMap 内部维护一个数组 Entry[] table, Entry 的 key 为 ThreadLocal 对象,value 为 ThreadLocal泛型对应的值。需要注意的是,Entry 使用 WeakReference< ThreadLocal>ThreadLocal对象变成一个弱引用的对象,这样在线程销毁时,对应的对象就会被回收,因此不会出现内存泄漏。 Entry[] table 就是最后存放数据的地方,其默认大小为 16,装载因子为 2/3(HashMap 的 load factor 为 0.75), 当大于等于容量的 2/3 的时候会重新分配 table。

既然 ThreadLocal 需要保存数据,必然涉及到数据的存和取,对应 ThreadLocal中的 set() 和 get() 方法。先来看 set() 方法:

set()
    public void set(T value) {
        Thread t = Thread.currentThread();
        //根据线程获取到其对应的 ThreadLocalMap 
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
           //第一次ThreadLocalMap 还没有创建时,会创建ThreadLocalMap 
            createMap(t, value);
    }
     private void set(ThreadLocal<?> key, Object value) {

            Entry[] tab = table;
            //拿到当前 table 的长度
            int len = tab.length;
            //计算出key的下标,通过 TheadLocal 的 hash 值去确定 Entry 该存放的位置
            int i = key.threadLocalHashCode & (len-1);

            //从计算出的下标开始循环:
            for (Entry e = tab[i];
                 e != null;
                 //使用 开放地址法(线性探测法) 解决散列冲突(HashMap 解决冲突的方式是 拉链法)
                 //开放地址法:发生哈希冲突时找到冲突位置的下一个位置,看能不能存放该key
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                //如果当前指向的 Entry 是存储过的 ThreadLocal,就直接将以前的数据覆盖掉,并结束
                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(); // 整理 + 扩容
        }
void createMap(Thread t, T firstValue) {
        //与线程进行绑定
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

set() 方法首先会获取到当前线程 t,然后通过 t 来获取当前线程中的ThreadLocal.ThreadLocalMap 对象。获取的方法就是:直接去当前Thread t 中访问。正如本文开头所示,在 Thread 类中有一个成员变量 ThreadLocal.ThreadLocalMap threadLocals = null;专门用于存储线程的ThreadLocal数据。这时候如果threadLocals不为 null,就将当前ThreadLocal对象和值 value 存入 Entry 中;否则,调用 createMap(t, value); 进行初始化,并把 value 放进去。在createMap(t, value)方法中,会与线程进行绑定,因此,不同线程的 ThreadLocalMap对象是不同的,这也是线程隔离的关键。

  • 每个 Thread 都有一个 ThreadLocalMap, 里面存放了这个 Thread 的所有 ThreadLocal
  • 这个 map 会在 ThreadLocal 设置值的时候懒加载,再将 ThreadLocal 作为 key 、要存的值作为 value 放入 map
  • 通过 ThreadLocal 的 hash 值来确定 value 放在 Map 的哪个位置
get()
    public T get() {
        Thread t = Thread.currentThread();
        //同样,根据线程获取到其对应的 ThreadLocalMap 
        ThreadLocalMap map = getMap(t);
        if (map != null) {
           //从map中取enttry,key为ThreadLocal对象
         // 通过 ThreadLocal 的 hash 值计算 index,index 取到的 key 值与 当前 ThreadLocal 一致则为目标值返回;
         //否则取下一个 index,直到取到的 entry 为空则直接返回空。
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                //获取到Entry中值
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
      private Entry getEntry(ThreadLocal<?> key) {
           //先根据 key 的哈希值,定位到 Entry 数组的下标
            int i = key.threadLocalHashCode & (table.length - 1);
             //然后获取  Entry 对象
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }
    private T setInitialValue() {
        T value = initialValue();  // return null
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
             //同样,第一次ThreadLocalMap 还没有创建时,也会创建ThreadLocalMap 
            createMap(t, value);
        return value;
}

同样,getMap() 方法同样会获取当前线程的 对应的ThreadLocalMap 对象 map。 如果 map 不为空,进一步获取通过getEntry()获取 ThreadLocalMap 对应的 Entry,再获取其中的 value;若为空,调用 setInitialValue()获取存储的数据。

通过分析 set()get() 方法,我们就可以知道 ThreadLocal是这样存储线程本地变量的:每个 Thread 内部都会维护着一个 ThreadLocalMap 变量,该变量可以看成是一个存储了 key 为 ThreadLocal、value 为所要存储的数据的 HashMap,当ThreadLocal存储 value 时,先通过当前 Thread 得到其对应的 ThreadLocalMap,然后将数据存储到该 map 中(实际存储在Entry中);而获取 value 时,仍然是先获取到当前 Thread 的 ThreadLocalMap,在获取到 ThreadLocalMap 中存储的value值。他们所操作的对象都是当前线程的 ThreadLocalMap 中的 table 数组,因此在不同线程中访问的都是同一个 ThreadLocalset()get(),他们对 ThreadLocal 的读写操作仅限于各自线程的内部,这就是为什么ThreadLocal可以在多个线程中可以互不干扰地存储和修改数据

Looer中的应用:

我们知道,一个线程只能有一个 Looper,为什么会这样呢?其实这与 ThreadLocal有关。看 Looper 中 sThreadLocal 的定义以及注释:

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

上面的注释清楚地说明,要想使用 Looper ,需要先调用 prepare() 方法(主线程中由于在应用启动时调用了prepareMainLooper(),因此主线程不需要再调用 prepare())。Looper有关的具体介绍可以参考另一篇博文:Android消息机制之Looper浅析。同时,注意到,这里 ThreadLocal的泛型为 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()方法,那么 sThreadLocal.get()就不为空,因此抛出异常,这就是不能多次调用 prepare()方法的原因。如果是第一次调用 Looper#prepare(),就会往 sThreadLocal中设置了 一个新创建的 Looper 对象,该对象会被保存在ThreadLocalMap中。

那么,Looer 对象是在哪里取的呢?我们知道与Looper#prepare()相对应的方法是Looper#loop(),那么就看看loop()方法:

  public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
   public static @Nullable Looper myLooper() {
       return sThreadLocal.get();
   }

开启loop循环的时候,myLooper() 方法会首先从 sThreadLocal取出我们在之前prepare()中设置的Looer对象,如果 Looer 对象为空,就说明ThreadLocal 中没有设置 Looer 对象,也就说明没有调用Looper.prepare()方法,因此会抛出我们常见的异常:"No Looper; Looper.prepare() wasn't called on this thread."。所以在调用Looper.loop()之前一定要先调用Looper.prepare()

总结:

  • 每个线程存在变量ThreadLocal.ThreadLocalMap threadLocals,保存着自己的 ThreadLocalMap

  • ThreadLocal 所操作的是当前线程的ThreadLocalMap 对象中的 table[] 数组,并把操作的 ThreadLocal 作为key 进行存储。

  • ThreadLocal 既不是为了解决共享多线程的访问问题,更不是为了解决线程同步问题,ThreadLocal 的设计初衷就是为了提供线程内部的局部变量,方便在本线程内随时随地的读取,并且与其他线程隔离。因此,ThreadLocal 是每个线程独享的,其数据别的线程不能访问,因此它是线程安全的。

  • Looer 的线程唯一性是因为 ThreadLocal中的ThreadLocalMap

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值