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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值