Android-ThreadLocal源码

一、ThreadLocal的一点个人理解

Android的消息机制主要是指Handler的运行机制,Handler的运行需要底层的MessageQueue和Looper的支撑。MessageQueue的中文翻译是消息队列,顾名思义它的内部存储了一组消息,其以队列的形式对外提供插入和删除的工作,虽然叫做消息队列,但是它的内部存储结构并不是真正的队列,而是采用单链表的数据结构来存储消息列表。Looper的中文翻译为循环,在这里可以理解为消息循环,由于MessageQueue只是一个消息的存储单元,它不能去处理消息,而Looper就填补了这个功能,Looper会以无限循环的形式去查找是否有新消息,如果有的话就处理消息,否则就一直等待着。Looper中还有一个特殊的概念,那就是ThreadLocal,ThreadLocal并不是线程,它的作用是可以在每个线程中存储数据。大家知道,Handler创建的时候会采用当前线程的Looper来构造消息循环系统,那么Handler内部如何获取到当前线程的Looper呢?这就要使用ThreadLocal了,ThreadLocal可以在不同的线程之中互不干扰地存储并提供数据,通过ThreadLocal可以轻松获取每个线程的Looper。当然需要注意的是,线程是默认没有Looper的,如果需要使用Handler就必须为线程创建Looper。大家经常提到的主线程,也叫UI线程,它就是ActivityThread,ActivityThread被创建时就会初始化Looper,这也是在主线程中默认可以使用Handler的原因。
ThreadLocal其实就是一个ThreadLocalMap的工具类,在每个线程中都有一个ThreadLocalMap实例,通过它可以在指定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,对于其它线程来说无法获取到数据。在日常开发中用到ThreadLocal的地方较少,但是在某些特殊的场景下,通过ThreadLocal可以轻松地实现一些看起来很复杂的功能,这一点在Android的源码中也有所体现,比如Looper、ActivityThread以及AMS中都用到了ThreadLocal。具体到ThreadLocal的使用场景,这个不好统一地来描述,一般来说,当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。比如对于Handler来说,它需要获取当前线程的Looper,很显然Looper的作用域就是线程并且不同线程具有不同的Looper,这个时候通过ThreadLocal就可以轻松实现Looper在线程中的存取,如果不采用ThreadLocal,那么系统就必须提供一个全局的哈希表供Handler查找指定线程的Looper,这样一来就必须提供一个类似于LooperManager的类了,但是系统并没有这么做而是选择了ThreadLocal,这就是ThreadLocal的好处。
ThreadLocal另一个使用场景是复杂逻辑下的对象传递,比如监听器的传递,有些时候一个线程中的任务过于复杂,这可能表现为函数调用栈比较深以及代码入口的多样性,在这种情况下,我们又需要监听器能够贯穿整个线程的执行过程,这个时候可以怎么做呢?其实就可以采用ThreadLocal,采用ThreadLocal可以让监听器作为线程内的全局对象而存在,在线程内部只要通过get方法就可以获取到监听器。而如果不采用ThreadLocal,那么我们能想到的可能是如下两种方法:第一种方法是将监听器通过参数的形式在函数调用栈中进行传递,第二种方法就是将监听器作为静态变量供线程访问。上述这两种方法都是有局限性的。第一种方法的问题时当函数调用栈很深的时候,通过函数参数来传递监听器对象这几乎是不可接受的,这会让程序的设计看起来很糟糕。第二种方法是可以接受的,但是这种状态是不具有可扩充性的,比如如果同时有两个线程在执行,那么就需要提供两个静态的监听器对象,如果有10个线程在并发执行呢?提供10个静态的监听器对象?这显然是不可思议的,而采用ThreadLocal每个监听器对象都在自己的线程内部存储,根据就不会有方法2的这种问题。
例如,不同的线程访问同一个ThreadLocal对象的时候,能获取到不同的值。主线程也是线程,跟子线程是一样的情况。能得到跟子线程不同的值。ThreadLocal中的数据获取,是需要通过当前线程来实现

二、ThreadLocal源码分析

ThreadLocal的构造器其实是一个空实现

1.ThreadLocal.set(T value)

    public void set(T value) {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 通过当前线程变量获取到线程实例中的ThreadLocalMap实例
        ThreadLocalMap map = getMap(t);
        // 如果Thread.threadLocals不为空,则保存数据
        if (map != null)
            map.set(this, value);
        else
            // 如果当前线程中的ThreadLocalMap实例为null,则初始化,并且向map中设置value
            createMap(t, value);
    }
    ThreadLocalMap getMap(Thread t) {
        // 是Thread中的ThreadLocalMap实例
        return t.threadLocals;
    }
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

2.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) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

3.ThreadLocal.remove()

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

4.ThreadLocal总结

其实ThreadLocal的代码比较简单,其操作的方法都是通过获取当前线程的ThreadLocalMap对象,然后操作ThreadLocalMap对象,对ThreadLocalMap对象进行set和get等操作

5.ThreadLocalMap分析

ThreadLocalMap是ThreadLocal的静态内部类

        static class Entry extends WeakReference<ThreadLocal<?>> {
          
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

ThreadLocalMap中的节点,是一个软引用实现类,并且使用ThreadLocal作为key,使用软引用,主要是为了避免内存溢出问题

(1)ThreadLocalMap.getEntry()
private Entry getEntry(ThreadLocal<?> key){
   // 计算hashCode与上数组的长度
   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);
}

getEnrty方法只会处理key被直接命中的entry,没有直接命中的(key冲突的)数据将调用getEntryAfterMiss()方法返回对应enrty,这样做是为了尽可能提升直接命中的性能。
1、计算Entry数组的index((length - 1) & key.hash)。
索引计算和HashMap的异同:
①相似之处:计算方式相同,均为(length - 1) & key.hash;length均为底层结构的大小(是大小,不是实际size)。即hash之后再hash计算一次得到key的index值。
②不同之处:HashMap(JDK8)底层数据结构是位桶+链表/红黑树,而ThreadLocalMap底层数据结构是Entry数组;HashMap的key.hash的计算方式是native、异或、无符号位移,(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16),ThreadLocalMap的key.hash从ThreadLocal实例化时便由nextHashCode()确定。
2、获取对应index的节点Entry;
3、如果返回节点entry 有值且其key未冲突(只有1个即entry返回的key等于传入的key),则直接返回该entry;
4、返回entry为空或键冲突,则调用getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)方法返回entry。

(2)ThreadLocalMap.getEntryAfterMiss
private Entry getEntryAfterMiss(ThreadLocal<?> key,int i, Entry e){
    Entry[] tab = table;
   int len= tab.length;
    while(e!=null){
        ThreadLocal<?> k = e.get();
       if(k== key)
           return e;
       if(k==null)
            expungeStaleEntry(i);
       else
     // 地址不等,切不为空,返回下一个。
            i= nextIndex(i, len);
        e= tab[i];
   }
   return null;
}

ThreadLocalMap之getEntryAfterMiss的流程:
仅分析Entry不为空的情况,
1、获取entry的key;
2、如果key一致(内存地址=判断),则返回该entry;
3、如果key为null,则调用expungeStaleEntry方法擦除该entry;
4、其他情况则通过nextIndex方法获取下一个索引位置index;
5、获取新index处的entry,再次循环2/3/4,直到定位到该key返回entry或者返回null。
private static int nextIndex(int i,int len){
return((i+1< len)? i+1:0); // 把索引加1即可
}

(3)ThreadLocalMap.expungeStaleEntry

只要key为null均会被擦除,使得对应value没有被引用,方便回收

private int expungeStaleEntry(int staleSlot){
    Entry[] tab = table;
   int len= tab.length;
    // expunge entry at staleSlot
    tab[staleSlot].value=null; // 擦除当前index处value
    tab[staleSlot]=null; // 擦除当前index处key
    size--;
    // Rehash until we encounter null
    Entry e;
   int i;
   for(i= nextIndex(staleSlot, len); // 计算下一个index
        (e= tab[i])!=null; // 新index处entry不为空
         i= nextIndex(i, len)){ // 计算下一个index
        ThreadLocal<?> k = e.get(); // 获取新key(ThreadLocal)
       if(k==null){ // key为null,再次置空
            e.value=null;
            tab[i]=null;
            size--;
       }else{
           int h= k.threadLocalHashCode&(len-1); // 计算新index
           if(h!= i){ // index若未变化,说明没有多余的entry了
                tab[i]=null;
                // Unlike Knuth 6.4 Algorithm R, we must scan until
               // null because multiple entries could have been stale.
// 一直扫到最后一个非空位置,将其值置为碰撞处第一个entry。
               while(tab[h]!=null)
                    h= nextIndex(h, len);
                tab[h]= e;
           }
       }
   }
   return i;
}
(4)
private void set(ThreadLocal<?> key, Object value){
    // We don't use a fast path as with get() because it is at
   // least as common to use set() to create new entries as
   // it is to replace existing ones, in which case, a fast
   // path would fail more often than not.
    Entry[] tab = table;
   int len= tab.length;
   int i= key.threadLocalHashCode&(len-1);
    // 遍历,判断是否已经存在key
    for(Entry e= tab[i]; e !=null;e= tab[i= nextIndex(i, len)]){
 // 当前index处已有entry
        ThreadLocal<?> k = e.get();
        if(k== key){ // key(ThreadLocal)相同,更新value
            e.value= value;
           return;
       }
        if(k==null){ // 出现过期数据
     // 遍历清洗过期数据并在index处插入新数据,其他数据后移
            replaceStaleEntry(key, value, i);
           return;
       }
   }
    tab[i]=new Entry(key, value);
   int sz=++size;
    // 没有过期数据被清理且实际size超过扩容阈值
   if(!cleanSomeSlots(i, sz)&& sz>= threshold)
        rehash();
}

rehash():其实就是重新计算table的大小,进行扩容
size:table的实际entry数量;扩容阈值threshold:table.lenrth(默认16)大小的2/3;
首先调用expungeStaleEntries删除所有过期数据,如果清理数据后size>=threshold的3/4,则2倍扩容。
ps:阈值又叫临界值,是指一个效应能够产生的最低值或最高值。阀fá 控制、开关、把持。

(4)ThreadLocalMap.resize
        private void resize() {
            // 缓存旧的table,并且将新的table长度比旧的长度扩大两倍
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;
            // 循环遍历旧table,将旧表中的Entry取出,
            // 然后依据新表的长度重新计算索引值,将旧表中的数据移到新数组中
            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                        int h = k.threadLocalHashCode & (newLen - 1);
                        // 如果该索引位置已经有value了,则向后移动一位进行保存
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }

            setThreshold(newLen);
            size = count;
            table = newTab;
        }

作为一个Android菜鸡,我觉得ThreadLocal还是需要同Handler一起结合来看比较好,这样能比较明白。
其实结合Handler,每个Thread中的ThreadLocalMap中保存了Looper(当然这个是因为Looper实例中的ThreadLocal实例作为key,如果有其他的ThreadLocal,其数据依然是保存在Thread中的ThreadLocalMap中),ThreadLocalMap中保存数据有多少个,是根据有多少个ThreadLocal实例来决定的,这也就是由操作数据的ThreadLocal来决定。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351