Android ThreadLocal特性以及源码解读

Android ThreadLocal特性以及源码解读

首先讲一下特性:ThreadLocal 以线程为作用域存取数据,不同线程有不同的数据副本,各个线程副本之间读取互不干扰
举个例子:

ThreadLocal<String> testThreadLocal = new ThreadLocal<>();
testThreadLocal.set("123");
Log.i("zx", "主线程中testThreadLocal值为" + testThreadLocal.get());

Thread thread1 = new Thread(new Runnable() {
    @Override
    public void run() {
        testThreadLocal.set("456");
        Log.i("zx", "thread1中testThreadLocal值为" + testThreadLocal.get());
    }
});
thread1.start();

Thread thread2 = new Thread(new Runnable() {
    @Override
    public void run() {
        Log.i("zx", "thread2中testThreadLocal值为" + testThreadLocal.get());
    }
});
thread2.start();

最终输出结果为

主线程中testThreadLocal值为123
thread1中testThreadLocal值为456
thread2中testThreadLocal值为null

结果确实符合上边对它的描述:主线程设置了值为 123,所以取出的值也是 123,thread1 中设置值为 456,所以取出 456,thread2 中没有设置值,所以取出 null。可以看到同一个对象 testThreadLocal,在不同的线程调用 get 方法,竟然能取出不同的值。ThreadLocal 的 set 和 get 到底做了什么操作能支持这样的特性呢?看看源码吧。

public void set(T value) {
    //获取set方法执行的线程
    Thread t = Thread.currentThread();
    //根据线程得到一个ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);//将数据放在ThreadLocalMap中
    else
        createMap(t, value);//ThreadLocalMap为空则新建一个带初始值的map
}

set 方法就这几行,看起来比较简单,重点是存放数据的 ThreadLocalMap,ThreadLocalMap 是通过 getMap() 方法获取到的,下边看看 getMap()

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

getMap()返回了线程的 threadLocals 属性,这个属性的类型是 ThreadLocalMap。也就是说每一个线程都有一个 ThreadLocalMap,在不同的线程中调用 set 方法,数据都是存放在每个线程自己的 ThreadLocalMap 里,所以就实现了存数据时互不影响。那由此推测,取数据时也是取各个线程自己的 ThreadLocalMap,下边来看看 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();
}

可以看到取值时仍然调用了 getMap() ,也就是说取值时仍然是从各个线程自己的 ThreadLocalMap 去取的,所以验证了我们的猜想。

再深入思考一下,如下的代码,ThreadLocal 该如何去存值呢?

{
  //同一线程下
  ThreadLocal<String> testThreadLocal1 = new ThreadLocal<>();
  ThreadLocal<String> testThreadLocal2 = new ThreadLocal<>();
  ThreadLocal<String> testThreadLocal3 = new ThreadLocal<>();

  testThreadLocal1.set("123");
  testThreadLocal2.set("456");
  testThreadLocal3.set("789");
}

之前我们已经知道了在同一线程下,数据最终肯定是放在同一个 ThreadLocalMap 里,那这里 set 操作并没有传一个唯一的 key,那取的时候如何从同一个 ThreadLocalMap 里取出不同的值呢?
再来看看 set 方法

public void set(T value) {
    //获取set方法执行的线程
    Thread t = Thread.currentThread();
    //根据线程得到一个ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);//将数据放在ThreadLocalMap中
    else
        createMap(t, value);//ThreadLocalMap为空则新建一个带初始值的map
}

关键操作在 map.set(this, value) 这一行,继续看看 ThreadLocalMap 类的 set()的源码

private void set(ThreadLocal<?> key, Object value) {
    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();
}

再来看看获取值时的过程,ThreadLocal 的 get()方法里有一行关键代码

ThreadLocalMap.Entry e = map.getEntry(this);

进入 ThreadLocalMap 类的 getEntry()方法,看看是否有计算索引值的代码

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

ThreadLocalMap 保存值的方法 set()和获取值的方法 getEntry() 都有这一行计算索引值的代码

key.threadLocalHashCode & (table.length - 1)//这里的key就是ThreadLocal对象

下边是生成 threadLocalHashCode 的代码,关键信息已经在注释里了

private final int threadLocalHashCode = nextHashCode();

//下一个要给出的哈希码,从零开始。
//由于是static类型,所以每次创建时会获取到上次递增之后的值,每次递增HASH_INCREMENT
private static AtomicInteger nextHashCode = new AtomicInteger();

//每次的增量
private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
    //等同于nextHashCode++,但是是线程安全的
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

以上的源码显示了,在每次创建 ThreadLocal 时会生成一个唯一标识 threadLocalHashCode, 这个 threadLocalHashCode 与存放数据的数组长度-1 做与运算,计算出的值作为存放数据的索引,每次取数据时同样使用这个索引。所以这就实现了同一个线程中,即使每次调用set()和get()时没有传一个key,也能根据ThreadLocal实例本身生成一个唯一的索引,这就保证了数据的正常读取。

总结一下:

数据并不是存放在ThreadLocal里,数据实际上是放在每个线程的ThreadLocalMap(threadLocals属性)中,只是使用ThreadLocal来管理数据。

同一个ThreadLocal在不同的线程中,他在每个线程的ThreadLocalMap中索引是相同的,但是ThreadLocalMap各不相同,所以实现了同一个ThreadLocal在不同线程中各有其值且互不干涉的特性;

同一个线程中不同的ThreadLocal,每个ThreadLocal实例创建时都有一个唯一标识符,并根据此标识符生成索引,根据此索引就可以实现在同一个ThreadLocalMap中读取。

如果我们需要以线程为作用域存取数据,不同线程有不同的数据副本时就可以考虑使用ThreadLocal。此外,熟悉ThreadLocal也可以方便我们理解Looper、ActivityThread等知识,这些地方都用到了ThreadLocal。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值