jdk1.8ThreadLocal底层源码详解

ThreadLocalMap结构

/**
         * 数组初始容量
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * 节点数组
         */
        private Entry[] table;

        /**
         * 数组里的entry数量
         */
        private int size = 0;

        /**
         * 阈值,扩容阈值
         */
        private int threshold; // Default to 0

底层源码解析

set方法分析
//threadLocal的set方法
//其实就是对threadLocal的内部类threadLocalMap类赋值,将它看成map
//threadLocalMap的内部类entry可以看成一组组的key-value
public void set(T value) {
        //获取当前线程,因为线程内部持有threadLocalMap
        Thread t = Thread.currentThread();
        //获取当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
  • 当前map是null的话,则创建一个新的map,然后做一些初始化设置
 void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            //INITIAL_CAPACITY默认16
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
  • 当前线程存在map了 那就开始对当前线程的map进行一系列的扫描清理,然后设值
    当前set方法大体流程图如下
//对当前key进行设值
private void set(ThreadLocal<?> key, Object value) {

            
            Entry[] tab = table;
            int len = tab.length;
            //对当前key算落入下标位置
            int i = key.threadLocalHashCode & (len-1);

            //对tab里面的脏数据进行扫描
            //直到碰到entry==null 停止
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                //如果已经存在相同key,用新值覆盖旧值,停止方法运行
                if (k == key) {
                    e.value = value;
                    return;
                }

                //有脏key进行回收和替换,并且扫描周边的脏key,停止方法运行
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            
            //没有脏key并且没有找到相同key则执行下方方法
            //新建entry
            tab[i] = new Entry(key, value);
            int sz = ++size;
            //cleanSomeSlots都没标记到周边的脏key,并且tab的length已经大于等于2/3len,就开始全表清理脏key,当大于等于1/2len才会开始扩容
            //因为可能有空key占用空间所以为了减少数据插入停滞则以2/3len为阈值
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
  • 在进行向后扫描的时候,如果找到和当前要设的key相同的key,则直接用新值覆盖,如果找到的key==null,也就是脏key,则replaceStaleEntry进行替换清理,并且做一系列的周边扫描
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            //staleSlot表示key==null的下标,默认将找到的key==null的下标作为要开始扫描的下标赋给了slotToExpunge
            int slotToExpunge = staleSlot;
            //向前查找找到更前面有key==null的脏数据重写赋给slotToExpunge,直到碰到tab[i]的entry是null停止
            //因为entry为空key说明那一格插槽没有脏数据了
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            //经过上一个for循环,已经定位到了最前一个脏数据的下标
            //下面这个循环则往后查找脏数据下标,直到遇到空的entry停止
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                //如果下一个key和要设置的key相同
                if (k == key) {
                    //对这个entry的值进行赋值
                    e.value = value;
                    //交换位置的值,把陈旧的entry赋值给next的tab
                    tab[i] = tab[staleSlot];
                    //则陈旧的entry用现在要插入的entry赋值,这样做的目的是可以把空key的entry往后挪扫描
                    tab[staleSlot] = e;

                    //只会进一次,也就是第一个for循环没找到脏数据,第二个for循环第一次key==k,后面也不会进去
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    //expungeStaleEntry清除空key数据,从slotToExpunge开始,里面还会清除附近的脏key
                    //cleanSomeSlots则是对刚才expungeStaleEntry清除停止的下标进行一个扩大范围的扫描清除,以n>>>1进行扫描清除
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                //往后扫碰到的key是null并且(向前查找的for循环没有定位到更前面的脏数据),则将要清理的slotToExpunge往后移
                //只会进一次,也就是第一个for循环没找到脏数据,第二个for循环第一次key==null,后面也不会进去
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            //陈旧的staleSlot值清除,方便GC回收
            tab[staleSlot].value = null;
            //如果第二个for循环没进入if (k == key) ,也就是没有设置过一样的key,则创建新的entry放到陈旧的下标上
            tab[staleSlot] = new Entry(key, value);

            //上方进入if (k == null && slotToExpunge == staleSlot) 一次后,清除下标则向后挪了一位,因为之前脏数据位已经被新的entry占用,所以不必清除staleSlot这位
            //expungeStaleEntry清除空key数据,从slotToExpunge开始,里面还会清除附近的脏key
            //cleanSomeSlots则是对刚才expungeStaleEntry清除停止的下标进行一个扩大范围的扫描清除,以n>>>1进行扫描清除
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }
  • expungeStaleEntry方法是对数据进行一个真正意义上的清理。具体就是对陈旧的值进行置空清理,容量减一。然后再开始扫描周边有没有脏key,如果有直接清理掉,如果key不是空,并且算出来的数组下表和旧的下标不一致,说明因为hash冲突引起的不断寻找空tab插入,所以会进行重新计算它的新位置,将旧位置的值清掉,然后再新位置上不断往后挪直到找到空地可插位置插入,则完成脏key的清理流程

        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            //陈旧的值置为空,方便GC回收
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            //值为空后,数组容量减一
            size--;

            Entry e;
            int i;
            //开始循环清理该陈旧值的后方旁边其它空值,知道碰到数组里面没有entry为止,因为没有entry说明还没创建对象,有entry没有key才是软引用过期的脏数据
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                //说明已经被回收,脏数据,val没被回收,所以这里收到置值为null
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    
                    int h = k.threadLocalHashCode & (len - 1);
                    //hash算完下角标跟它取出来的位置不一样说明是因为它算出来的那个值已经hash冲突,导致它往后插入而引起不一样
                    if (h != i) {
                        //帮当前位置这个值置空掉,将之前数据赋值给新算出的下标位置
                        tab[i] = null;

                        //如果算出来的位置又有entry了,再找下一个,直到找到空的位置将entry设置进去。对该entry,重新进行hash并解决冲突
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            //返回的是循环清理的最后一个值,也就是碰到数组entry为空的前一个entry的下角标值
            return i;
        }
  • cleanSomeSlots方法主要对脏key进行一个扩大范围的扫描,然后对脏key进行标记,由expungeStaleEntry处理掉,对n进行右移,如果有脏key则标记removed清除,然后调用expungeStaleEntry开始清理脏key,直到n==0,这里并没有全部清除而是类似于折中清除,提高性能
//清空一些插槽的空key,但是这个方法并没做真正清理
//而是对空key进行标记,在expungeStaleEntry进行清除
//(n >>>= 1) != 0这个是控制log2n查找,并没有全部查找,在性能和脏数据之间做了一个折中
//只要找到一个值,又重新给n赋值,又开始log2n直到,n == 0 为止抽的这些都没有脏数据
//扩大扫描范围
private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                //有key被回收,脏数据,则进行expungeStaleEntry清理
                if (e != null && e.get() == null) {
                    //调用cleanSomeSlots方法的时候的n跟现在的数组的长度可能会不一样,所以这是扩大扫描范围
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            //只要查找的对象entry的key不为空,也就是没有一个脏数据,返回值就是false,也就是没有移除数据
            return removed;
        }
  • 重新计算hash,当然在要扩容之前要对
//扩容,对hash进行重新计算
private void rehash() {
            //全表脏key清理
            expungeStaleEntries();

            //当容量大于等于1/2len开始扩容
            if (size >= threshold - threshold / 4)
                resize();
        }
  • 全表数据清理
//对tab的全表进行一个数据清理
//因为调用这个方法是在扩容,扩容前进行全表清理
private void expungeStaleEntries() {
            Entry[] tab = table;
            int len = tab.length;
            for (int j = 0; j < len; j++) {
                Entry e = tab[j];
                if (e != null && e.get() == null)
                    //数据清理
                    expungeStaleEntry(j);
            }
        }
  • 扩容,当前容量达到旧的长度的一半,则开始扩容,容量变成原来的两倍,然后旧表的脏key的值置null,如果不是脏key则开始往新的表转移,重新计算hash取新数组索引。然后再重新设置阈值和容量
//扩容,为原理的2倍
private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;

            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 {
                        //重新计算hash,新数组下标值
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }
            //设置新阈值
            setThreshold(newLen);
            //设置新容量
            size = count;
            //将新数组替换旧数组
            table = newTab;
        }
  • 设置阈值
//设置阈值
private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }
get方法
  • 流程图
  • 源码解析
public T get() {
        Thread t = Thread.currentThread();
        //获取map
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //获取entry节点
            ThreadLocalMap.Entry e = map.getEntry(this);
            //entry节点不是空说明已经创建过了,直接从entry获取值返回就可以
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //map是空或者map不是空entry节点是空会在这里进行初始化
        return setInitialValue();
    }
//获取entry节点
private Entry getEntry(ThreadLocal<?> key) {
            //计算数组索引位置
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            //key和当前位置的key一样,就直接返回entry
            if (e != null && e.get() == key)
                return e;
            //可能因为hash冲突导致插入的值不在计算位置上
            //依次往后查找,并标记清除脏key
            else
                return getEntryAfterMiss(key, i, e);
        }
//可能因为hash冲突导致插入的值不在计算位置上
//依次往后查找,并标记清除脏key
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;
       }
//如果都没找到对应entry和值,则对当前线程进行一个初始化
//里面的方法和set类似,就不做过多解释了
private T setInitialValue() {
        //没有子类重写的话为null
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
remove
//threadLocal数据清除
public void remove() {
        //获取map
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             //调用localmap的remove方法
             m.remove(this);
     }
private void remove(ThreadLocal<?> key) {
            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对象是同一个,就清除引用对象,并开始调用expungeStaleEntry清理陈旧的值以及周边的值
                //expungeStaleEntry这个方法已经在set里解析过了,这边就不多说
                if (e.get() == key) {
                    //就是把reference指向null,也就是将该entry的key指向null。方便后面对该entry进行清理
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

总结

代码其实并不难,唯一的难点在set方法线性探测法的扫描,脏key的处理,从看源码到画流程图还是挺不容易的,喜欢的朋友可以点点赞,哈哈哈!!!

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

推荐阅读更多精彩内容