Java ThreadLocal深究

    最近在研究EventBus的时候碰到一个ThreadLocal的使用场景,考虑到Handler里面也用到了这玩意,比较重要和高端,所以研究下,先来看个Demo:

package testthreadlocal;

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadLocalDemo {

    //一个可以用原子方式更新的int值
    private static final AtomicInteger ai = new AtomicInteger(0);

    //ThreadLocal对象
    static ThreadLocal<Integer> local = new ThreadLocal<Integer>() {
        protected Integer initialValue() {
            return ai.getAndIncrement();
        };
    };

    public static void main(String[] args) {
        //创建5个线程,每个线程都有一个id和他绑定,注意,这个id是我们
        //强行和他绑定的一个int数据而已,并不是系统为这个线程分配的id
        for (int i = 0; i < 5; i++) {
            new Thread() {
                public void run() {
                    System.out.println("线程" + Thread.currentThread().getName() 
                            + " 的ID是:" + local.get());
                };
            }.start();
        }
    }
}

    上面的例子中首先创建了一个ThreadLocal,这个ThreadLocal存储的是一个Integer类型的数据;然后简单的调用了AtomicInteger的getAndIncrement方法对ThreadLocal进行了初始化;接着创建了5个线程,每个线程都可以自由访问这个ThreadLocal;最后5个线程都去ThreadLocal取里面存储的Integer的值,然后输出结果如下:

线程Thread-0 的ID是:1
线程Thread-2 的ID是:2
线程Thread-3 的ID是:3
线程Thread-4 的ID是:4
线程Thread-1 的ID是:0

    可以看到,对于同一个成员变量local里面存储的值,不同的线程获取的结果不一样,有没有感觉好神奇?local看起来非常简单,在创建的时候就是简单的调用了初始化函数:initialValue;都没有set方法,然后get出来的值就不一样了,这么厉害的吗?要想分析这种现象的原因,就必须研究ThreadLocal的代码,不啰嗦了,123,上源码,先从initialValue方法开始分析:

//默认的initialValue方法的实现就是返回空
//在上例中,是将AtomicInteger原子自增
protected T initialValue() {
        return null;
}

    initialValue方法很简单,实际上这个方法是根据我们的需求进行重写的,下面分析get方法,这是重点:

   public T get() {
        //获取线程,就是调用get方法,在上例中,我们创建了5个子线程,每
        //个子线程都调用了get,那么这个t就分别指向了刚刚创建的5个子线程
        Thread t = Thread.currentThread();

        //ThreadLocalMap是一个自定义的HashMap,不过他没有继承自HashMap
        //而是自己实现了一个,他的作用仅仅是保存线程本地变量,getMap就是获
        //取跟线程绑定的ThreadLocalMap,Thread类里面有这样的成员变量;每
        //个Thread的子类在创建并初始化的时候就会给这个变量赋值
        ThreadLocalMap map = getMap(t);

        //如果ThreadLocalMap不为空,那么进入if获取对应线程的值
        if (map != null) {
            //根据ThreadLocalMap获取Entry,
            //注意哦,传进去的是TheadLocal
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }

        //如果ThreadLocalMap为空,或者Entry为空
        //那么进入setInitialValue去设置初值
        return setInitialValue();
    }

    通过get方法,我们可以得到如下的图:
ThreadLocal3

    可以看到,每个线程都有一个ThreadLocalMap对象,这是一个Map,键是ThreadLocal,值是我们要存储的值。get方法的第一步就是拿到线程的Map;然后根据传进来的ThreadLocal对象去Map里面找Entry,如果找到了,那么拿到此Entry的value,这个value正是我们要获取的值;如果没有拿到Entry,那么调用setInitialValue去初始化一个Entry,并返回初始的value值。下面分析setInitialValue方法
    下面首先来分析getEntry方法:

private Entry getEntry(ThreadLocal<?> key) {
    //获取此key的hashcode,并与entry数组的长度 - 1做
    //与操作,其实质跟HashMap中获取桶的索引是一样一样的
    int i = key.threadLocalHashCode & (table.length - 1);

    //拿到数组元素Entry
    Entry e = table[i];

    //如果数组元素不为空,而且key一样,那么返回此元素
    if (e != null && e.get() == key)
        return e;
    else
        //如果元素不存在,或者存在,但是和目标key不一样
        //也就是说此线程的Map并没有保存传进来的ThreadLocal
        return getEntryAfterMiss(key, i, e);
}

    getEntry方法比较简单,就是根据key的hash和数组长度 - 1做与运算算出此key的数组索引(这个key是ThreadLocal对象),注意,这里ThreadLocal的hash是通过一个原子自增的int型数据来实现的。索引拿到了,那么数组元素自然就得到了。如果此数组元素Entry不为空,而且此Entry的key和目标key一样,那么直接拿到这个Entry的value返回即可;如果Entry为空,或者Entry不为空但是此Entry的key和目标的key不一样,那么直接调用getEntryAfterMiss,下面分析这个方法:

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    //拿到数组和长度
    Entry[] tab = table;
    int len = tab.length;

    //如果元素存在,但是key不一样,可能存在hash碰撞,也有可能是
    //key为空;如果是hash碰撞,那么使用探针法一个个比对数组元素
    while (e != null) {
        //调用Entry的get方法获取ThreadLocal值
        ThreadLocal<?> k = e.get();

        //如果此ThreadLocal等于传进来的,那么返回
        if (k == key)
            return e;

        //如果此key为null,那么擦除这个Entry
        //因为key是弱引用,很容易被干掉;如果
        //被干掉了,那么对应的value也要被干掉
        if (k == null)
            expungeStaleEntry(i);
        else
            //如果key不为空,那么去
            //数组的下个元素查找值
            i = nextIndex(i, len);
        e = tab[i];
    }
    //如果探针法还没找到元素Entry
    //说明此ThreadLocal压根就没有
    //被保存进此线程的Map,那么返空
    return null;
}

    getEntryAfterMiss方法也比较简单,进入这个方法的前提是在ThreadLocalMap里面没有找到key和目标key一样的Entry。发生此现象的原因有三种:
    1.Entry压根就不存在,也就是说,Map里面没有保存此ThreadLocal,这种情况对应上面代码的最后一行:return null;
    2.Entry存在,key也存在,但是key不一样,说明发生了hash碰撞,这个时候就用探针法一个个去拿到数组的元素比对,这种情况对应上面代码的i = nextIndex(i, len);
    3.Entry存在,但是key为空,这就说明Entry里面存储的ThreadLocal会回收了,因为Entry里面的key是一个ThreadLocal弱引用,当ThreadLocal被回收时,key就为空了,这时候就要把这整个Entry擦除掉,因为留着他也没有意义了。

    下面看下擦除Entry的方法expungeStaleEntry:

        //擦除value的操作,参数staleSlot代表第几个数组元素
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            //擦除元素,并将数组元素个数自减,
            //因为key已经为null了,所以无需操作key
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            //当遇到key是空的情况,需要重新hash;因为数组
            //元素的内存必须是连续的,一旦擦除一个元素,那么
            //此数组元素的内存就不连续了,重新hash就是为了保
            //这种内存的连续性
            Entry e;
            int i;
            //往死里遍历此数组
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                //拿到key值
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    //如果碰到key为空的情况,那么
                    //将相应的value和Entry置空
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    //重新计算Entry的索引
                    int h = k.threadLocalHashCode & (len - 1);

                    //如果新的索引和老的索引不一样,
                    //那么将老索引对应的Entry置空
                    if (h != i) {
                        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);

                        //最后确定新的索引后,将Entry放进去
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

     expungeStaleEntry方法的思路是根据索引拿到数组的元素Entry,然后把这个Entry的value和他本身都置空;置空后,数组元素的内存就不连续了,此时从置空的那个索引开始遍历后面的元素,如果又发现key为空的情况,那么继续擦除Entry,方法同上;碰到key不为空的情况,重新计算这个Entry的索引,如果老的索引和新的索引不一样,那么将此Entry放入新的索引,如果新的索引本来就有元素Entry了,那么继续使用探针法查找新的索引,一直到找到为止,然后将此Entry放进去。
     通过上面几个方法的分析,我们知道了ThreadLocal的get方法里面的Entry是怎么拿到的,玩意这个Entry没拿到怎么办?这种情况就说明此ThreadLocal是第一次放入此线程的ThreadLocalMap中,那么就调用setInitialValue进行初始化,下面看看:

//从命名来看,是设置初始值
    private T setInitialValue() {
        //调用initialValue,这个方法是需要自
        //己重写的,上例中就是将ai进行了原子自增。
        T value = initialValue();

        //获取调用get方法的线程
        Thread t = Thread.currentThread();

        //根据线程获取ThreadLocalMap,也就是
        //获取Thread内部的ThreadLocalMap变量
        ThreadLocalMap map = getMap(t);

        //如果ThreadLocalMap,那么调用set方法设置具体的值;一般
        //来说,在线程调用init方法的时候,都会对此Map进行初始化
        if (map != null)
            map.set(this, value);
        else
            //如果map实在不存在,那么创建此Map
            //注意两个参数,一个是当前线程,另一个是value
            createMap(t, value);

        //将设置的值返回回去
        return value;
    }

    可以看到,setInitialValue的作用就是首选调用initialValue整出一个初始值来,这个值是要放入线程的ThreadLocalMap的;然后获取此线程的ThreadLocalMap;如果ThreadLocalMap不为空,那么调用他的set方法将ThreadLocal和setInitialValue算出来的值当做一个键值对放进此ThreadLocalMap;如果此ThreadLocalMap为空,那么调用createMap创建一个ThreadLocalMap,在创建的时候就将键值对传进去,createMap和getMap方法非常简单,不单独分析。至此,ThreadLocal的get方法分析完毕,下面用图形来表示这个过程:
ThreadLocal4

    有取就有存,下面分析下set方法,看看ThreadLocal是怎么存储数据的:

public void set(T value) {
    //首先拿到调用set的线程
    Thread t = Thread.currentThread();
    //根据线程,拿到此线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);

    //如果Map不为空,直接set
    if (map != null)
        map.set(this, value);
    //如果Map为空,那么创建
    else
        createMap(t, value);
}

    ThreadLocal的set方法本身是很简答的,他的实现思路是:
    1.首先获取调用线程。
    2.其次获取此线程的ThreadLocalMap。
    3.如果ThreadLocalMap不为空,那么直接set
    4.如果为空,那么创建Map,创建的时候就把ThreadLocal和value传进去保存了。
    看得出,ThreadLocalset方法的核心还是ThreadLocalMap的set方法,下面分析:

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.
    //首先拿到ThreadLocalMap的元素数组
    Entry[] tab = table;

    //数组长度
    int len = tab.length;

    //根据key(ThreadLocal类型)计算此键值对应该存放的索引
    int i = key.threadLocalHashCode & (len-1);

    //从上面计算出来的索引开始遍历数组
    //因为可能产生hash碰撞,此时需要指
    //针探测,所以需要遍历数组
    for (Entry e = tab[i];
        e != null;
        e = tab[i = nextIndex(i, len)]) {

        //拿到Entry的key,也就是ThreadLocal类型的对象
        ThreadLocal<?> k = e.get();

        //如果此Entry的key和目标的key一样
        //那么直接更新这个Entry的值就好了
        if (k == key) {
            e.value = value;
            return;
        }

        //流程执行到这里,意味着发生了hash碰撞

        //遍历过程中,发现存在key为空的Entry,那么需要擦除他
        //擦除完了会退出for循环,因为在擦除过程中会进行for循环
        //,遍历i后面的数组元素,直到找到key相同的Entry,或者
        //没找到,此时就会创建新的Entry插入到这个key为空的位置
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    //如果没发现此key,也没有key为null的Entry,
    // 那么创建一个Entry存进ThreadLocalMap
    tab[i] = new Entry(key, value);

    //数组数量自增
    int sz = ++size;

    //清除key为空的Entry,如果没有这样的Entry,但是数组个
    //数有超出了阈值,那么调用rehash进行扩容,容量是原来两倍
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

    LocalThreadMap的set方法的思路是:
    1.根据key算出数组索引。
    2.以算出来的索引为起点,向后遍历数组
    3.如果碰到key相同的Entry,那么更新值并返回
    4.如果碰到key为空的Entry,那么调用replaceStaleEntry并返回
    5.如果3和4都没碰到,那么创建一个全新的Entry并插入数组里面
    6.如果走的是5流程,那么清理可能存在key为空的Entry;而且如果数组满了,那么调用rehash进行扩容

    整个过程用图形表示如下:
ThreadLocal5

    下面分析replaceStaleEntry方法:
//从命名都能看出,这是替换废弃的Entry,所谓的废弃的Entry,是指key为null的Entry
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    //拿到数组和数组长度
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    //数组索引,这个索引上的Entry的key为空,同时要以这
    //个索引为起点,清理此索引后面所有的key为空的Entry
    int slotToExpunge = staleSlot;

    //以这个索引的前一个索引为起点往前遍历此数组
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))

         //如果发现此Entry的key为空,那么将索引赋值给
         //slotToExpunge;也就是说清理的起点发生了变化
         if (e.get() == null)
             slotToExpunge = i;

         // Find either the key or trailing null slot of run, whichever
         // occurs first
         //继续遍历数组,以key为空的Entry的索引的下一个索引为起点,向后遍历
         for (int i = nextIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();
  
            //如果找到key相同的Entry,那么就替换值
            if (k == key) {
                e.value = value;

                //索引为staleSlot的Entry的key是空的,这里把那个key为空
                //的Entry移到遍历到的索引,同时将遍历到的Entry放入那个key
                //为空的数组索引里面,说白了就是交换数组里面的元素
                tab[i] = tab[staleSlot];
                tab[staleSlot] = e;

                // Start expunge at preceding stale entry if it exists
                //如果两个值相等,说明传进来的索引的前面没有key为null的Entry
                if (slotToExpunge == staleSlot)
                    slotToExpunge = i;

                //这个时候就需要擦除key为空的Entry了,不过如果传进来的索引
                //的前面有key为空的Entry,那么从前面的索引为起点开始清理
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                return;
            }

        //如果在传进来的索引的后面又发现了空key的Entry,
        //但是前面没有,那么重新记录清理的起点
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
        }

        // If key not found, put new entry in stale slot
        //如果遍历一圈没找到key相同的Entry,说明是第一次存储此
        //ThreadLocal,那么新建一个Entry存入传进来的索引里面
        tab[staleSlot].value = null;
        tab[staleSlot] = new Entry(key, value);

        // If there are any other stale entries in run, expunge them
        //两个值不等,说明传进来的索引的前面或者后面有key为空的
        //Entry,那么以slotToExpunge为起点,清理key为空的Entry
        if (slotToExpunge != staleSlot)
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }
}

    首先注意调用replaceStaleEntry方法的前提,那就是在遍历数组的过程中,发现有一个Entry的key为空,那么将此Entry的索引和要存储的键值对传进去,这个方法的设计思路如下:
    1.记录key为空的索引为slotToExpunge,意思是这个索引后面的数组元素都要遍历一遍,以便清除key为空的Entry。
    2.以传进来的索引的前一个索引为起点,遍历数组,看看有没有key为空的Entry,如果有,那么将其索引赋值给slotToExpunge,这意味着清理的起点变了。
    3.以传进来的索引的后一个索引为起点,遍历数组,看看有没有key和传进来的key相同的Entry,如果有,那么首先更新此Entry的值;然后将传进来的那个key为空的Entry存入这个位置,最后将这个Entry存入传进来的那个位置。如果传进来的前面没有key为空的Entry,那么更新清理的起点为当前的索引;最后以slotToExpunge为起点开始清理key为空的Entry,然后返回。
    4.如果遍历了一圈发现没有找到key和传进来的key一样的Entry,那么创建一个新的Entry,将他存放到传进来的那个索引里面

    5.如果可能,就清理key为空的Entry。可能的条件是slotToExpunge != staleSlot,说明除了传进来的索引以为,还有别的索引上有key为空的Entry,用图表示如下:
ThreadLocal6

    如果遍历一圈,发现没有key相同的Entry,怎么办?那么就创建一个Entry,放到上图第一个数组key为空的那个索引里面即可。以上就是set方法的执行过程。

    可以看到,set方法是把值set到了每个线程中,get方法是从每个线程的TheadLocalMap中获取值,ThreadLocal本身只是拿到线程的TheadLocalMap,然后通过这个Map去get和set,ThreadLocal的实现核心还是每个线程的TheadLocalMap。

    除了get和set方法,还有remove方法,remove方法特别简单,首先查找key相同的Entry,然后调用他的clear方法将数据清空,接着调用expungeStaleEntry方法清理数组,上面都分析过,不单独分析了。

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

推荐阅读更多精彩内容