ThreadLocal源码剖析

1.ThreadLocal概述

本文源码基于android 27

1.1 简介

ThreadLocal提供了线程内的局部变量,这个局部变量是只存在于当前线程的。同时,它是独立于其他线程,即其他线程无法访问。这样就不会存在线程安全的问题了。

1.2 常用方法

    public void set(T value)  //设置当前线程中变量的副本
    public T get()            //获取在当前线程中保存的变量副本
    public void remove()      //移除当前线程中变量的副本

2.ThreadLocal源码分析

下面逐一对这三个方法的源码进行分析。

2.1 ThreadLocal的set()源码分析

直接看源码吧~

2.1.1 ThreadLocal的set()

    public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取一个map
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //map不为null的话,以ThreadLocal.this为key,保存值
            map.set(this, value);
        else
            //map为null,则直接创建map
            //注意,这里传的是当前线程t
            createMap(t, value);
    }

set()方法中就是先获取一个ThreadLocalMap对象,如果这个ThreadLocalMap不为null的话就直接把数据保存到这个map中,否则的话先创建map出来。
所以,ThreadLocal的数据本质是保存到一个map中。需要注意,这个map是以ThreadLocal.thiskey来保存值的。

然后我们来看下这里面调用到的三个方法:

  • getMap(t)
  • createMap(t, value)
  • map.set(this, value)

2.1.2 ThreadLocal的getMap()

    ThreadLocalMap getMap(Thread t) {
        //返回当前线程t中的一个成员变量threadLocals
        return t.threadLocals;
    }
public class Thread implements Runnable {
     //...
     ThreadLocal.ThreadLocalMap threadLocals = null;
     //...
}

可以看到,这个map实际上是Thread的一个成员属性。即map跟线程绑定在一起了。这就可以解释了为何ThreadLocal只是线程的局部变量了。

2.1.3 ThreadLocal的createMap()

    void createMap(Thread t, T firstValue) {
        //还是以ThreadLocal.this为key来保存值
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

就是new一个ThreadLocalMap出来。
我们来看看这个ThreadLocalMap是怎么样的。

2.1.4 ThreadLocal的ThreadLocal类

ThreadLocalMapThreadLocal的内部类。

    static class ThreadLocalMap {
        //Entry继承自弱引用
        //GC时一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
        //即回收后,ThreadLocal这个key值就会变成null
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            //使用ThreadLocal作为key
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        //初始容量为16
        private static final int INITIAL_CAPACITY = 16;
        
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            //创建一个容量为16的数组
            table = new Entry[INITIAL_CAPACITY];
            //根据key的哈希值算出索引位置
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            //创建一个Entry,并将其放入数组中
            table[i] = new Entry(firstKey, firstValue);
            //存放元素的个数设为1
            size = 1;
            //调整阈值的大小,这个是用来判断扩容的
            setThreshold(INITIAL_CAPACITY);
        }
    }

需要注意的是,ThreadLocalMap里面的Entry继承了弱引用,这个很重要,后面的一系列操作都是基于这个弱引用去实现的。那么为什么要使用弱引用呢?这是因为使用弱引用能够减少内存的使用。我们知道,弱引用很容易给回收,这样就能够让ThreadLocalMap保持尽量的小。

同时,也可以看到,这个ThreadLocalMapHashMap这些不一样,没有去实现Map接口。ThreadLocalMap内部是由数组去实现的,并且其key只能是ThreadLocal类型。

2.1.5 ThreadLocalMap的set()

再来看下ThreadLocalMapset()方法:

        private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            
            //这里是使用nextIndex,即索引加1来解决哈希碰撞的
            //跟HashMap不同,HashMap是使用链表来解决
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                //如果Entry已存在相同的key,覆盖掉旧值
                if (k == key) {
                    e.value = value;
                    return;
                }
                //如果k为null,则替换过期的Entry
                //这是因为Entry继承了弱引用后,GC后就会出现k为null的情况
                if (k == null) {
                    //替换过期的Entry
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //如果在数组中都没找到已存在的key或者null值,则新建一个加入到数组中
            tab[i] = new Entry(key, value);
            int sz = ++size;

            //由于弱引用,key值可能为null,因此先要清理掉这些没用的Entry,再去判断数组的大小有没超过阈值
            //如果不用清除Entry并且达到阀值,那么就执行扩容操作
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();//重新调整位置,如果超过阀值,就扩容
        }

这里的set()方法,实际就是对以下三种情况做处理:

  1. map中已存在相同的key值,则直接覆盖掉旧的值;
  2. 如果根据哈希后的索引位置在map中找到一个key值为nullEntry,则替换掉这个无用的Entry,具体操作看下面的分析;
  3. 以上两种情况都没,则直接新建Entry添加到map中,同时执行清除没用的Entry以及考虑是否要扩容。

2.1.6 ThreadLocalMap的replaceStaleEntry()

replaceStaleEntry()的作用是替换掉过期没用的Entry,看下面的分析:

        //key:set的key值
        //value:set的value值
        //staleSlot:过期Entry的索引位置
        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            //擦除位置指向过期Entry的索引位置
            int slotToExpunge = staleSlot;
            //数组往前找到第一个不为null的Entry
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    //擦除位置指向找到的key为null的索引位置
                    slotToExpunge = i;

            //从过期Entry的索引位置开始往后找
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                //如果在遍历中找到跟要set的key值相等的,那么交换它们
                //因为ThreadLocalMap使用了nextIndex来解决哈希碰撞,即要set的key值可能会出现在当前索引的后面
                //因此,这个key存在的话,则应该找出来,擦除掉,并在当前索引中设置新的
                //注意,当前索引位置tab[staleSlot]的key值为null
                //这里采用了交换的方式去实现
                if (k == key) {
                    //设置新值,并交换数据
                    e.value = value;
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    //如果擦除位置指向过期Entry的索引位置
                    //那么修改为交换后的索引位置
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    //expungeStaleEntry:擦除从指定位置开始的一些过期数据,并重新调整位置
                    //cleanSomeSlots:清理一段过期的数据
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                //找到下一个为null的key值
                if (k == null && slotToExpunge == staleSlot)
                    //擦除位置指向新的
                    slotToExpunge = i;
            }

            //数组中没找到相同key的话,则直接新建一个放进数组当前索引位置中
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            //如果存在其他的过期数据,那么清理掉这一段的过期数据
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

替换过期数据同样也可以分为两种情况:

  1. 由于哈希碰撞的关系,先从当前过期数据的索引位置往后找,如果能找到相同key的话,就设置新值,并交换它们的位置;
  2. 如果上面的过程找不到相同的key,即原来的map中没有这一key值,那么直接新建一个键值对,放到当前过期数据的索引位置中。

这一期间会包含清理过期数据的操作。
下面来看下这两个方法:

  • expungeStaleEntry(slotToExpunge)
  • cleanSomeSlots(expungeStaleEntry(slotToExpunge), len)

2.1.7 ThreadLocalMap的expungeStaleEntry()

expungeStaleEntry()的作用是擦除从指定位置开始的一些过期数据,并重新调整位置。

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

            //擦除staleSlot指向的过期数据,
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            //往后找,知道遇到null为止
            //擦除k == null的数据
            //重新调整k != null的位置
            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;
        }

expungeStaleEntry()同样可以分为以下三步:

  1. 先擦除当前指定位置的数据;
  2. 然后往后遍历,如果遇到key值为null的数据就擦除掉,并看下key值不为null的数据能不能挪下位置。因为哈希碰撞的关系,一些数据会存放到比较后的位置,如果前面出现空位,那么将它们往前移一下,可以提高一下查找速度。
  3. 如果遇到Entrynull,则退出循环。

2.1.7 ThreadLocalMap的expungeStaleEntry()

expungeStaleEntry()的作用是清理一段过期的数据。

        private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                //往后加1的索引位置
                i = nextIndex(i, len);
                Entry e = tab[i];
                //找到要擦除的数据
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    //expungeStaleEntry见上面的分析
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);//执行(log2 n)+1次循环
            return removed;
        }

while ( (n >>>= 1) != 0)控制了循坏次数,所以有可能只扫描了这个数组的其中一段;当然也有可能整个都扫描了。这个要看要擦除的数据是否比较多了,因为找到要擦除的数据后,n会重新被赋值为len

2.1.8 小结

至此,ThreadLocal.set()所涉及到的都分析完毕了。

简单总结一下,在每个线程内部都有一个成员变量threadLocals,这个是threadLocalsThreadLocal.ThreadLocalMap类型,它能够保存以ThreadLocalkey的数据。因此,一个线程中是可以有多个不同的ThreadLocal对象的。

2.2 ThreadLocal的get()源码分析

我们再来看下get()方法:

2.2.1 ThreadLocal的get()

    public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取map
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //以ThreadLocal.this为key获取Entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                //从Entry中获取value值返回
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果map为null或Entry为null,会初始化一个null值返回
        return setInitialValue();
    }

通过get()来获取值,如果map中存在这个key值就返回对应的value值;否则就初始化一个null值返回。
分别来看下以下这两个方法:

  • map.getEntry(this)
  • setInitialValue()

2.2.2 ThreadLocalMap的getEntry()

        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            //如果根据哈希后的索引位置能找到的话,直接返回
            //否则调用getEntryAfterMiss继续找
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

如果能根据哈希后的索引位置找到Entry的话,就直接返回;否则的话就调用getEntryAfterMiss继续找。

2.2.3 ThreadLocalMap的getEntryAfterMiss()

        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
            //同样是使用nextIndex来查找下一个,直到null为止
            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    //找到返回
                    return e;
                if (k == null)
                    //如果找到有null值,擦除一发
                    expungeStaleEntry(i);
                else
                    //继续找下一个
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

同样,由于哈希碰撞的关系,往后找。能找到就返回结果,否则就返回null

2.2.4 ThreadLocalMap的setInitialValue()

再来看下setInitialValue(),就是初始化value然后返回。

    private T setInitialValue() {
        //初始化Value值,为null
        T value = initialValue();
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取map
        ThreadLocalMap map = getMap(t);
        //如果map不为null,就把null值设置进去
        //如果map为null,先创建一个map,并且也把null值设置进去
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

再来看下initialValue()

2.2.5 ThreadLocalMap的initialValue()

    protected T initialValue() {
        //返回null,
        return null;
    }

就是返回一个默认值:null。需要注意的是,这个是protected方法,如果需要修改这个默认的返回值,可以继承之后去重写。

2.2.6 小结

get()方法很简单,就是从map中找,能找到就返回结果,找不到就话就返回一个默认的值:null。另外,我们可以通过重写initialValue()去修改这个默认返回值。

2.3 ThreadLocal的remove()源码分析

如果我们不使用ThreadLocal了,可以使用remove()来移除掉。
来看下源码~

2.3.1 ThreadLocal的remove()

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         //如果Map不为null,从Map中移除以以ThreadLocal.this为key的键值对
         if (m != null)
             m.remove(this);
     }

再来看下ThreadLocalMapremove()

2.3.2 ThreadLocalMap的remove()

        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            //通过nextIndex来逐一遍历,如果找到就清除掉
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    //清除key值
                    e.clear();
                    //替换过期的Entry
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

remove()过程还是很简单。

2.3.3 其他

虽然在使用set()get()时有概率会进行一些清理回收操作。但是还是建议在不使用时,手动调用一下remove()方法,避免出现内存泄露。

3.其他的一些问题

3.1 ThreadLocal为什么会内存泄漏?

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现keynullEntry,就没有办法访问这些keynullEntryvalue,如果当前线程再迟迟不结束的话,这些keynullEntryvalue就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

推荐一篇文章:深入分析 ThreadLocal 内存泄漏问题

3.2 为什么ThreadLocal要使用弱引用?

官方文档的说法:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了应对非常大和长时间的用途,哈希表使用弱引用的 key。

补充一下:使用弱引用能够减少内存的使用。因为弱引用很容易给回收,这样就能够让ThreadLocalMap保持尽量的小。

3.3 如何防止弱引用被回收从而找不到值?

可以使用static来修饰ThreadLocal,从而延长ThreadLocal的生命周期。但是并不能保证不会内存泄漏。

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

推荐阅读更多精彩内容