ThreadLocal源码以及ThreadLocalMap数据结构和原理解析

简述及用法

ThreadLocal是java中的一个线程存储类,它能够实现在指定线程内存储数据,而且在该线程中只能获取和修改该线程存储的数据,使用方式很简单:


ThreadLocal mThreadLocal =new ThreadLocal<>();

mThreadLocal.set("");

mThreadLocal.get();

接下来就去看看它的源码

ThreadLocal

先看一下ThreadLocal的构造方法:


public 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();

}

首先根据当前线程获取到一个ThreadLocalMap,再以自身为参数获取到一个Entry类型的对象e,而这个evalue值就是我们想要获取的数据,如果e为空就调用setInitialValue()函数并返回其返回值;

这个ThreadLocalMap是通过getMap函数获取的


ThreadLocalMapgetMap(Thread t) {

  return t.threadLocals;

}

可以看出这个 ThreadLocalMapThread的一个成员变量,ThreadLocalMap的数据结构其实是一个数组,该数组的元素是Entry类型

看一下Entry的数据结构


static class Entry extends WeakReference<ThreadLocal<?>> {

    Object value;

    Entry(ThreadLocal k, Object v) {

        super(k);

        value = v;

    }

}

Entry是一个k-v类型的数据结构,且key固定为ThreadLocal类型,并且对该类型对象的引用为弱引用。

这时应该就能得知,调用ThreadLocal.get函数其实就是从调用的线程中取出ThreadLocalMap,再以ThreadLocal为key找到ThreadLocalMap中对应的Entry,最终返回其value,如果entry为空的话,会调用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;

}

这里是先通过initialValue函数返回默认的value,再获取到线程的ThreadLocalMap,如果不为空的话,就将默认的value存入,否则为该线程初始化ThreadLocalMap并存入默认的value(ThreadLocal类中的initialValue返回的为null,我们可以通过继承来重写该方法设置默认的value)。

接着看一下ThreadLocalset函数


public void set(T value) {

    Thread t = Thread.currentThread();

    ThreadLocalMap map = getMap(t);

    if (map !=null)

        map.set(this, value);

    else

        createMap(t, value);

}

可以看出set函数和setInitialValue函数只有value的区别,前者使用的是传给它的参数,后者用的是默认的。

接下来要着重分析一下ThreadLocalMap这个类

ThreadLocalMap

ThreadLocalMap是一种哈希表,既然是哈希表,那么就一定有它的哈希算法以及处理哈希冲突的方法。

ThreadLocalMap的元素类型为Entry类型,它是ThreadLocalMap的静态内部类,有keyvalue,且key为弱引用;

ThreadLocalMap的装载因子阈值为2/3

ThreadLocalMap的哈希算法为:


key.threadLocalHashCode & (len-1);

而在ThreadLocal中:


private final int threadLocalHashCode =nextHashCode();


private static int nextHashCode() {

    return nextHashCode.getAndAdd(HASH_INCREMENT);

}


private static AtomicInteger nextHashCode = new AtomicInteger();


private static final int HASH_INCREMENT =0x61c88647;

可以看出,每新建一个ThreadLocal的实例,获取它的threadLocalHashCode时都会在nextHashCode 上累加0x61c88647(通过CAS),为什么是0x61c88647呢,官方给出的注释是说为了能让哈希码能够均匀的散列在长度为2^n的数组中,详细原理我也没怎么研究。
不过我写了一个测试Demo,从0累加16次0x61c88647,再&15

    public void threadLocalHash(){
        int code = 0;
        for (int i = 0; i < 16; i++) {
            code += 0x61c88647;
            System.out.print((code & 15) + "  ");
        }
    }

得到结果如下:



未发生一次哈希冲突,太神奇了,听说这与斐波拉契散列法黄金分割有关。

HashMap不同,ThreadLocalMap是通过开放地址法来处理哈希冲突的,且是线性探测法,方法特点是:当发生哈希冲突时,顺序查看表中的下一个单元,直到找出一个空闲单元或遍历完整个表

由于对 key的引用为弱引用,所以表中很有可能会存在key=null的数据,这些数据被称为脏数据(或者叫过期数据,无效数据都行,我个人喜欢这么称呼),脏数据是已经无法再使用的数据,但还占据着数组单元,会造成内存的浪费,所以ThreadLocalMap在查询,添加,删除以及扩容的过程中都会穿插清理脏数据的过程,清理脏数据主要涉及到三个比较重要的函数:


1.replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot);

2.expungeStaleEntry(int staleSlot);

3.cleanSomeSlots(int i, int n);

下面详细讲讲这三个函数做了哪些事

replaceStaleEntry


private void replaceStaleEntry(ThreadLocal key, Object value, int staleSlot) {

    Entry[] tab =table;

    int len = tab.length;

    Entry e;

    int slotToExpunge = staleSlot;

    for (int i =prevIndex(staleSlot, len);

        (e = tab[i]) !=null;

        i =prevIndex(i, len))

        if (e.get() ==null)

            slotToExpunge = i;

    for (int i =nextIndex(staleSlot, len);

        (e = tab[i]) !=null;

        i =nextIndex(i, len)) {

        ThreadLocal k = e.get();

        if (k == key) {

            e.value = value;

            tab[i] = tab[staleSlot];

            tab[staleSlot] = e;

            if (slotToExpunge == staleSlot)

                slotToExpunge = i;

            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);

             return;

        }

        if (k ==null && slotToExpunge == staleSlot)

            slotToExpunge = i;

    }

    tab[staleSlot].value =null;

    tab[staleSlot] =new Entry(key, value);

    if (slotToExpunge != staleSlot)

        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);

}

顾名思义,该方法的作用就是替换脏数据,tab[staleSlot]为脏数据,需要替换成键为key,值为valueEntry,就调用这个函数。

该函数主要做了两件事:替换和清除

替换规则为:

1、从staleSlot开始往后遍历看能不能找到键为key的数据tab[i]直到tab[i]为空,如果存在就将其值改为value,并且与tab[staleSlot]交换位置,此时tab[i]为脏数据;

2、如果不存在键为key的数据,就直接将tab[staleSlot]改成新数据。

也很好理解,就是考虑到数组可能存在键为key的数据,但发生了哈希冲突所以不在其哈希值所对应的下标位置,所以就要向后遍历,如果在遇到数据为空之前没有遍历到键为key的数据就说明表中没有键为key的数据,直接替换掉tab[staleSlot]即可,否则如果遍历到tab[i]的键为key,就会将其值改为value,并且与tab[staleSlot]交换位置,此时,tab[i]就为脏数据。

在以下三种情况下会执行清除操作:

1、首先从staleSlot向前遍历,在遇到空数据之前如果遇到了脏数据就会进行一次清除;

2、第一种替换规则,说明一定存在脏数据;

3、替换过程中向后遍历寻找键为key的数据时遇到了脏数据。

以上三种情况满足一种都会进行清楚操作:

cleanSomeSlots(int i, int n);

不过在replaceStaleEntry中是这样调用的:


cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);

所以首先应该看一下expungeStaleEntry方法:


private int expungeStaleEntry(int staleSlot) {

    Entry[] tab =table;

    int len = tab.length;

    tab[staleSlot].value =null;

    tab[staleSlot] =null;

    size--;

    Entry e;

    int i;

    for (i =nextIndex(staleSlot, len);

        (e = tab[i]) !=null;

        i =nextIndex(i, len)) {

        ThreadLocal k = e.get();

        if (k ==null) {

            e.value =null;

            tab[i] =null;

            size--;

        }else {

            int h = k.threadLocalHashCode & (len -1);

            if (h != i) {

                tab[i] =null;

                while (tab[h] !=null)

                    h =nextIndex(h, len);

                tab[h] = e;

            }

        }

    }

    return i;

}

同样通过函数名可以得知该函数的作用是删除脏数据,replaceStaleEntry方法中传入的参数为最靠前的脏数据的下标,在这个方法中,首先会将tab[staleSlot]置空,并将tab长度减一,既然tab[staleSlot]为空,如果后面还有数据是因为哈希冲突向后挪的,就需要补上staleSlot的位置,于是该方法中会从staleSlot向后遍历,在遇到空数据之前,如果遇到脏数据就要清除并将tabsize减一,如果不是脏数据,那么会对该数据重新做一次哈希运算,如果他所在的位置是哈希就是运算后的下标值,那么就什么都不做,否则就给他重新分配下标,并将原位置置空。

最后该方法会返回遍历到的空数据的下标,假设为i,所以执行一次该方法可以保证数组中从staleSloti不存在脏数据

再回到cleanSomeSlots方法:


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];

        if (e !=null && e.get() ==null) {

            n = len;

            removed =true;

            i = expungeStaleEntry(i);

        }

    }while ( (n >>>=1) !=0);

    return removed;

}

它会对expungeStaleEntry返回值i下标后面进行扫描,扫描次数为logn(以2为底n的对数)次,每次扫描主要做的事是:

i为基准值向后遍历一位,如果不为脏数据,那么i的值就会向后推一位,再进行下一次扫描,如果为脏数据,则对这一位调用expungeStaleEntry,并且将返回值赋给i,再进行下一次扫描。该过程中如果清除了脏数据,则返回true,否则返回false

至此为止,三个重要的函数的原理都分析完了,再带着这些去看ThreadLocalMap的添加、删除、扩容的等方法就会容易理解很多

首先看一下set方法:


private void set(ThreadLocal key, Object value) {

    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 k = e.get();

        if (k == key) {

            e.value = value;

            return;

        }

        if (k ==null) {

            replaceStaleEntry(key, value, i);

            return;

        }

    }

    tab[i] =new Entry(key, value);

    int sz = ++size;

    if (!cleanSomeSlots(i, sz) && sz >=threshold)

        rehash();

}

很容易理解,首先计算元素哈希值,再根据对应下标进行线性探测,如果遇到了key值相同的数据则替换value,如果遇到null则直接占据,如果遇到脏数据就替换,当然还有可能会进行一次脏数据清除。如果是线性探测将占据了一个空位置的情况下,则说明数组元素增加了一个,那么就会从该位置开始调用cleanSomeSlots进行扫描清除,如果未清除脏数据且元素个数大于阈值,则会执行一次rehash方法:


private void rehash() {

    expungeStaleEntries();

    if (size >=threshold -threshold /4)

        resize();

}

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

    }

}

就是先清除掉整张表的脏数据,然后在判断数组中元素值是否超过了阈值的3/4,如果是则进行扩容:


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 {

                int h = k.threadLocalHashCode & (newLen -1);

                while (newTab[h] !=null)

                    h =nextIndex(h, newLen);

                newTab[h] = e;

                count++;

            }

        }

    }

    setThreshold(newLen);

    size = count;

    table = newTab;

}

扩容是二倍扩容,然后再将所有元素重新散列到新表上(如果遇到脏数据就不用散列了,直接将value置空等gc自己回收就完事了)。

get方法:


private EntrygetEntry(ThreadLocal key) {

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

}

private EntrygetEntryAfterMiss(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;

}

同样是计算key的哈希值,再在数组中看对应下标是否是键为key的数据,如果是就直接返回,否则线性探测,过程中如果遇到了目标元素就返回,遇到脏数据就会调用expungeStaleEntry进行清除,遇到null则说明数组中不存在目标数据,直接返回null

remove方法:


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)]) {

        if (e.get() == key) {

            e.clear();

            expungeStaleEntry(i);

            return;

        }

    }

}

线性探测获取到键为key的数据,将key置空以后再清除。

所以说ThreadLocalMap只要搞懂了脏数据的清除规则其他的也就很容易理解了。

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