ThreadLocal 实现原理总结

ThreadLocal 用于在不同线程中互不干扰的存储并提供数据。

这里不对源码进行深究,只浅显的对实现原理进行了解。

本次涉及到的源码为 Source for Android 27.


ThreadLocal 的实现,需要借助到 ThreadLocalMap

需要提前交代的:
在一个 Thread 实例内部,都有一个 threadLocals 成员变量(ThreadLocalMap 类型),而这个 threadLocals 内部又维护了一个 Entry 类型的数组。

Entry 是一个 key - value 实体,用于保存 ThreadLocal - Object 键值实体。


先看一段代码:

@Override
protected void onCreate(Bundle savedInstanceState) {
    setContentView(R.layout.activity_main3);
    
    ThreadLocal<String> threadLocal = new ThreadLocal<>();
    
    new Thread(() -> {
        threadLocal.set("Thread 1");
        Log.d("Test", "Thread 1 => "+threadLocal.get());
    }).start();
    
    threadLocal.set("Main");
    Log.d("Test", "MainThread => "+threadLocal.get());

}

在这段代码里面,生成了一个 ThreadLocal<String> 实例 threadLocal,然后分别在 UI 线程和一个子线程里面去分别进行 set()get() 操作。然后,就会在不同线程里面打印对应的值。


首先看 threadLocal.set() 方法内部:

//	ThreadLocal.set()
public void set(T value) {
    Thread t = Thread.currentThread();//获得调用 set() 方法所处的线程实例,即 Thread 实例
    ThreadLocalMap map = getMap(t);//进一步获得 Thread 实例的 threadLocals(ThreadLocalMap 类型) 成员变量
    if (map != null)
        map.set(ThreadLocal.this, value);
    else
        //如果 t 的 threadLocals == null,则新建一个
        //新建的时候会将当前 ThreadLocal 的 this 引用传递进去
        createMap(t, value);
}

//	ThreadLocal.getMap()
ThreadLocalMap getMap(Thread t) {
    //每个 Thread 实例都会有一个 threadLocals 成员变量
    return t.threadLocals;
}

在某一线程里面调用 threadLocalset(value) 方法,那么 Thread.currentThread() 就会得到当前线程的实例,然后通过该 Thread 实例获取到其内部的成员变量 threadLocalsThreadLocalMap 类型),然后将当前 threadLocal 实例作为 key 值,与形参 value 绑定在一起生成一个 Entry 实例(当然这里是通常情况,不考虑重复的 key 而替换原值的情况),并存储到 ThreadLocalMap 内部的 Entry[] 中。

//	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();
        // 如果是重复的 key 值,则替换原有的 value 值
        // 这就对应着在同一个线程调用两次 threadLocal.set() 情况
        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.set(value) 就将 threadLocal 实例与 value 绑定在一起存放在了当前线程(即 Thread 实例)之中(这是从模糊的概念上来说),或者可以说,当前线程根据 threadLocal 实例作为索引,可以存储对应的 value 值。

只要理解了 threadLocal.set(value) 大致的原理,那么对于 threadLocal.get() 方法也容易理解了。

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

threadLocal.get() 的时候,就会根据当前线程( Thread 实例)得到对应的 threadLocalsThreadLocalMap 类型),再进一步得到 Entry[]key 值为 threadLocalEntry 实例,最后获得 Entry 实例的 value 值。


上述,就是 ThreadLocalset(value)get() 的大致实现,虽然有点绕,但是仔细体会一下,还是容易理解的。

彩蛋部分:
另外,我在自己琢磨到似懂非懂的状态时,就突然产生了一个疑问,为什么 ThreadLocalMap 内部要维护一个 Entry[] (数组),而不是单个的 Entry 实例,因为明明只要存储 threadLocal-value 的键值对。

后来,突然一下就想明白了,看了下面的代码,也许就能体会到了。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main3);
    
    ThreadLocal<Boolean> threadLocal1 = new ThreadLocal<>();
    ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
    
    new Thread(() -> {
        threadLocal1.set(true);
        threadLocal2.set("Thread 1");
        Log.d("Test", "Thread 1 => "+threadLocal1.get());
        Log.d("Test", "Thread 1 => "+threadLocal2.get());
    }).start();
    
    threadLocal1.set(false);
    threadLocal2.set("Main");
    Log.d("Test", "MainThread => "+threadLocal1.get());
    Log.d("Test", "MainThread => "+threadLocal2.get());
}

对于每一个线程(即 Thread 实例)来说,可能遇到需要维护多个不同的 threadLocal-value 的情况(即会产生多个不同的 Entry 实例),因此,就需要一个 Entry[] 来保存多个 Entry 实例。


最后,还需要说明的一点是,ThreadLocal 的作用是为不同的线程存储对应的对象(实际上是对象的引用,想想 String 类型),如果在两个线程中存储的都是同一个对象的引用,那么在两个线程中得到也必然会是同一对象的引用,这点是需要注意的。

public class Test {
    int anInt;
    public static void main(String[] args) {
        Test t = new Test();
        t.anInt = 3;
        ThreadLocal<Test> local = new ThreadLocal<>();
        local.set(t);
        local.get().t(10);
        new Thread(() -> {
            local.set(t);
            System.out.println(local.get().anInt);
        }).start();
        System.out.println(local.get().anInt);
    }
    public int t(int v)  {
        anInt = v;
        return anInt;
    }
}

打印结果:
10
10


最后,还是总结一下,ThreadLocal 的大致实现原理:

在某一线程 thread1 中调用 threadLocal1.set(value) 时,实际上是把该 threadLocal1value 以健值对的形式存储在 thread1 中(更进一步的说是一个健值对数组中,因为 thread1 可能对应多个 ThreadLocal 对象)

然后调用 threadLocal1.get() 方法时,实际上会先得到调用该方法时的线程(通过 Thread.currentThread() 方法实现)thread1,然后通过 thread1 且以 threadLocal1 自身为 key 从健值对数组中得到 threadLocal1 对应的 value

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值