ThreadLocal原理

我们通常用ThreadLocal来实现线程局部变量的存储。在许多开源框架中ThreadLocal被广泛使用。这篇文章来讨论一下ThreadLocal的实现原理

一、ThreadLocal的实现原理

ThreadLocal的实现原理其实很简单,我们先来看一下ThreadLocal常用的几个方法:

public void set(T value)
public void remove()
public T get()

通过set、get、remove这个三个方法,可以实现线程局部变量的添加、获取、删除。
首先我们先来看一下set方法的实现:

  public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

我们可以看到set方法是将对象value保存在当前线程的threadLocals这个ThreadLocalMap中,以当前的ThreadLocal对象作为map的键值。ThreadLoccaMap是ThreadLocal类的一个内部类,我们先不管它的实现,先来看一下ThreadLocal的get方法和remove方法的实现。

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

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

我们可以看到,当当前线程的ThreadLocalMap对象存在的时候,返回值从这个对象中获取,这个和set方法保存value是相对应的,都是从当前线程保存的ThreadLocalMap对象中存储和获取。当ThreadLocalMap对象不存在的时候返回 setInitialValue() 返回的对象。这个方法我们可以看到:通过 initialValue()方法获得一个value对象,这个方法默认返回一个null,留给子类实现,用来初始化一个用来保存的对象的默认值。然后将这个对象放在当前线程的ThreadLocalMap中(如果不存在ThreadLocalMap对象就创建一个)。

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

依然是调用ThreadLocalMap的remove方法。
好了,究竟ThreadLocalMap是什么?我们接下来看一下:
其实ThreadLocalMap是一个Map,它的实现和HashMap类似,我们先来看一下用来保存K-V的节点:

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

我们先不用管WeakReference是什么,我们只需要知道,Entry 保存了k和v。
接下来看一下ThreadLocalMap的set方法的实现:

        private Entry[] table;

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

ThreadLocalMap的set方法和早期HashMap的实现类似,都是先计算哈希,然后确定hash槽的位置,不同的是,ThreadLocalMap通过数组存储K-V对象(Entry),而HashMap是通过散列表存储K-V对象。ThreadLocalMap首先获得存储数组的长度,然后通过hash算法计算要设置的节点所在的哈希槽的位置,如果哈希槽的位置没有元素,就新创建一个Entry对象放在这里。如果有元素,就判断该元素的k是否和当前要设置的k相等,如果是就将这个哈希槽存储的entry对象的value重新赋值;如果k是空的话,说明这个ThreadLocal对象被手动设置为null了,是无效的。就把这个节点替换掉,具体怎么实现看一下replaceStaleEntry这个方法,这里不赘述。如果当前哈希槽位置有合法元素,并且k不和要保存的k相等,就去下一个哈希槽的位置重复检查,下一个哈希槽的位置是这个方法计算的:

  private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

添加完元素之后,会去判断当前存储数组内元素的数量是否超过了threshold,我们可以叫threshold为扩容因子,threshold = len * 2 / 3。当超过扩容因子的时候就去检查并且移除坏节点。移除坏节后,如果size >= threshold - threshold / 4,就要真正的扩容。我们看一下这个方法:

private void rehash() {
    expungeStaleEntries();
    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();
}

expungeStaleEntries()这个方法,从方法名上可以看出这个方法的作用:去除坏掉的Entries。什么是坏掉的Entries呢?我们可以看一下这个方法的实现:

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

当一个节点不是null时,调用节点的get()方法,如果说得的结果是null,这个节点就是坏的节点。Entry的get方法其实是他父类WeakReference<T>的父类Reference<T>的方法。这两个类是什么呢?对JVM有了解的小伙伴应该对这两个类不陌生,我们知道,在java中,对象的引用分为四种:强引用、软引用、弱引用、虚引用。引用强度逐渐减弱。
强引用是我们常见的对象引用,比如:Object o = new Object();
只要一个对象被强引用所引用,就不会被垃圾收集器回收。当内存不足时,jvm会抛出OOM异常。
软引用对应着Reference<T>的实现类SoftReference<T>,这种引用引用的对象不会立刻被回收,但是当内存空间不足的时候,垃圾收集器就会回收软引用所引用的对象。
弱引用对应着Reference<T>的实现类WeakReference<T>,当软引用指向的对象被垃圾收集器发现后,就会回收这个对象(只有软引用引用这个对象,如果强引用同时也引用这个对象时,这个对象并不会被回收)。
虚引用:也叫幽灵引用,虚引用主要用来跟踪对象被垃圾回收器回收的活动。不会影响任何垃圾回收的过程。
回到前面的所说的判断是否为坏的节点,e.get()所获得的其实是e存储的key,也就是ThreadLocal对象。所以我们可以看出,Thread中的ThreadLocalMap并不会影响ThreadLocal在jvm中的生命周期。当一个节点被判定为坏节点后,这个节点就会被移除,具体实现我们看一下expungeStaleEntry这个方法:

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

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            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;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

首先将位置为staleSlot处存储的Entry的对象的value也设置为null,然后将这个对象从存储的数组中移除,并将size减1。然后一次检查下一个位置(nextIndex(staleSlot, len)确定下一个位置)是否为坏节点,是的话就移除,否则重新计算这个节点所在的位置,将这个节点移动到计算后的新位置。这样做的原因是因为在set节点的时候,如果存在hash冲突,并且key不相等时,会将调用nextIndex(staleSlot, len)方法重新确定hash槽的位置。
真正的扩容方法是由resize()方法实现的,实现过程是很简单。我们看一下具体实现:

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

我们可以看到,每次扩容大小都是原来大小的2倍,扩容的过程就是新建一个大小为原来2倍的数组,将原来数组内的元素放到新数据中。过程很简单这里就不在赘述。

二、疑问,线程池中,大量任务使用ThreadLocal会不会造成OOM

根据上面的分析,ThreadLocal实现线程局部存储是通过每个线程Thread中的ThreadLocalMap存储以ThreadLocal对象为key,以要存储的对象为value来实现的。而ThreadLocalMap中,存储K-V是通过Entry实现,Entry继承了WeakReference。所以ThreadLocalMap不会影响ThreadLocal对象的在内存中的回收。通过之前《java线程池浅析》这篇我们可以知道线程池实现的原理其实是多个(或一个)线程执行提交的runnable任务。runnable任务中使用ThreadLocal,当runnable任务结束(其实是run方法结束),runnable任务中使用的ThreadLocal就会失去和GCroot的连接,这个时候只有ThreadLocalMap中的Entry会引用该ThreadLocal对像,所以当内存不足的时候,ThreadLocal对像会被回收。所以在线程池中,这种ThreadLocal的使用是不会造成OOM的。

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

推荐阅读更多精彩内容