源码解读 ・ ThreadLocal

匿名 (未验证) 提交于 2019-12-03 00:22:01

        第一次知道ThreadLocal是在看Looper源码的时候知道的,那时候只知道它的作用是让数据在各个线程单独保持一份,互不干扰,也一直没有去研究它的具体实现。昨天下班前粗略地看了一遍,我心里想的是“这玩意儿真的是太麻烦了,要是我的话,直接在线程里维护一个Object数组就能实现这个功能啊”。然后下了班回到家,我又仔仔细细的看了一遍,果然大佬还是你大佬,我还是太天真了。


        在正式读代码前先简单介绍ThreadLocal的实现方式。每个线程都会有一个ThreadLocalMap,只有在使用到ThreadLocal的时候才会初始化ThreadLocalMap。需要存储的对象T会被放到Entry里面存储在ThreadLocalMap的数组中,Entry是一个键值对的数据结构,ThreadLocal实例为key,T为value。在使用的过程中,ThreadLocal会先找到当前线程的ThreadLocalMap,根据ThreadLocal的散列值找到存储的位置执行get方法或者set方法。

        下面我画了一张图来说明。当定义ThreadLocal<A>对象,在不同线程中保存的A实例对象分别保存在各自线程的ThreadLocalMap中。


        老样子,还是由一段简单的代码开始深入源码

final ThreadLocal<String> threadLocal = new ThreadLocal<>(); threadLocal.set("你好"); Log.d("mark", "mark_1:" + threadLocal.get()); new Thread(new Runnable() {     @Override     public void run() {         Log.d("mark", "mark_2:" + threadLocal.get());         threadLocal.set("很高兴见到你");         Log.d("mark", "mark_3:" + threadLocal.get());     } }).start();
05-31 16:58:30.878 19235-19235/com.newhongbin.lalala D/mark: mark_1:你好 05-31 16:58:30.879 19235-19626/com.newhongbin.lalala D/mark: mark_2:null 05-31 16:58:30.879 19235-19626/com.newhongbin.lalala D/mark: mark_3:很高兴见到你

        首先在主线程中创建ThreadLocal对象,并set“你好”,在主线程中get,可以看到取到的就是刚才set的字符串;然后开启一个子线程,这时候子线程中还没有set过,所以取出来的是null,在子线程set过之后,就能够成功取出相应的字符串了。虽然是同一个ThreadLocal对象,但是在不同的线程中get到的数据是不一样的。


set

        顺着set方法一探究竟:

public void set(T value) {         Thread t = Thread.currentThread();         ThreadLocalMap map = getMap(t);         if (map != null)             map.set(this, value);         else             createMap(t, value);     }

        逻辑很简单,取到当前线程的ThreadLocalMap,如果没有初始化过,就调用createMap初始化。初始化过程就是调用ThreadLocalMap的其中一个构造方法,我们来看看这个构造方法。

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

        构造方法中会定义一个Entry数组,数组初始化容量为16,扩容因子为2/3,每次扩容为原来的2倍。Entry是WeakReference的子类,因此ThreadLocal不会影响存储对象的生命周期以及内存回收。Entry实现了键值对存储的功能,当前ThreadLocal对象为key,需要存储的对象为value。

        初始化完Entry数组之后,需要计算当前ThreadLocal的散列值(hashcode),因为这里是第一个放入的Entry,所以不可能会发生hash碰撞,所以计算完hashcode之后,就直接把Entry放入数组下标为hashcode的位置上。最后计算出下一次需要扩容的临界值,即 (数组长度*2/3) 。到此为止,第一个值成功set。

        那么问题又来了:

        1、如果一个ThreadLocal在同一个线程中多次set呢?

        2、如果多个ThreadLocal在同一个线程中的hashcode一样怎么办呢?

        OK,回到刚开始的set方法,如果ThreadLocalMap不为null的情况。

    private void set(ThreadLocal key, Object value) {          Entry[] tab = table;         int len = tab.length;         int i = key.threadLocalHashCode & (len-1);         //发生hash碰撞时如果碰撞的位置上已经有Entry,且原有的key没有被回收,就查找数组下一个位置,如果没有Entry就放入         for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {                          ThreadLocal k = e.get();              if (k == key) {//同一个ThreadLocal多次set,会直接覆盖原来的值                 e.value = value;                 return;             }              if (k == null) {//原来的ThreadLocal已经被回收了,就放入新的Entry                 replaceStaleEntry(key, value, i);                 return;             }         }         //在空的位置上放入Entry之前先判断是否需要扩容         tab[i] = new Entry(key, value);         int sz = ++size;         if (!cleanSomeSlots(i, sz) && sz >= threshold)             rehash();     }

        set方法中关键的几个步骤我都在源码中加了注释,应该比较容易理解,这样就能回答上面的两个问题:

        1、如果一个ThreadLocal在同一个线程中多次set呢?

             直接覆盖原有的值。

        2、如果多个ThreadLocal在同一个线程中的hashcode一样怎么办呢?

             如果发生碰撞的那个位置上的Entry的ThreadLocal被回收了,就放在碰撞的位置上;如果没有被回收,就寻找Entry下一个位置进行判断,直到找到ThreadLocal被回收的Entry,或者空的位置,放入。


get

        前面的set其实已经把ThreadLocal的基本实现方式全部梳理清楚了,所以接下来的get方法看起来会更容易些。

public T get() {     Thread t = Thread.currentThread();     ThreadLocalMap map = getMap(t);     if (map != null) {         ThreadLocalMap.Entry e = map.getEntry(this);         if (e != null)             return (T)e.value;     }     return setInitialValue(); }

        如果当前线程在get之前已经初始化过ThreadLocalMap,那么就根据hashcode找到指定的Entry,返回value。

        如果当前线程在get之前还未初始化过ThreadLocalMap,那么就返回setInitialValue()。

private T setInitialValue() {     T value = initialValue();     Thread t = Thread.currentThread();     ThreadLocalMap map = getMap(t);     if (map != null)         map.set(this, value);     else         createMap(t, value);     return value; }  protected T initialValue() {     return null; }
        setInitialValue方法很简单,定义一个null,如果ThreadLocalMap不为空,就插入null;如果ThreadLocalMap为空,先调用createMap初始化ThreadLoaclMap,再插入null。最后返回null。

转载请标明出处:源码解读 ・ ThreadLocal
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!