Java ThreadLocal深究

作者: 涂豪_OP | 来源:发表于2018-08-13 14:46 被阅读124次

        最近在研究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方法清理数组,上面都分析过,不单独分析了。

    相关文章

      网友评论

        本文标题:Java ThreadLocal深究

        本文链接:https://www.haomeiwen.com/subject/hgtrbftx.html