Java ThreadLocal源码分析(基于API 29 JDK8)

ThreadLocal是Thread的局部变量,用于多线程编程,接下来看一个简单的例子:

public class Example {
    private final ExecutorService service = Executors.newFixedThreadPool(2);
    private final ThreadLocal<String> threadLocal = new ThreadLocal<>();

    @Test
    public void main() {
        threadLocal.set("main");
        System.out.println(threadLocal.get());
        service.execute(runnable1);
        service.execute(runnable2);
    }

    private Runnable runnable1 = new Runnable() {
        @Override
        public void run() {
            threadLocal.set(Thread.currentThread().getName());
            System.out.println(threadLocal.get());
        }
    };

    private Runnable runnable2 = new Runnable() {
        @Override
        public void run() {
            System.out.println(threadLocal.get());
        }
    };
}

/*******************运行结果
main
pool-1-thread-1
null
*******************/

从上面的例子中可以看到ThreadLocal的全局对象在主线程中设置为main字符串,子线程如果不设置值默认为null,设置值则有自己独立的值。查看其构造函数:

public class ThreadLocal<T> {
    public ThreadLocal() {
    }
}

空构造函数,基本什么都没做,接着看它的成员方法set/get:

    public void set(T value) {
        // 当前线程
        Thread t = Thread.currentThread();
        // 获取当前线程的threadLocals成员
        ThreadLocalMap map = getMap(t);
        // map不为空,直接set
        if (map != null)
            map.set(this, value);
        else
            // map为空执行初始化
            createMap(t, value);
    }

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

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

    public T get() {
        // 当前线程
        Thread t = Thread.currentThread();
        // 获取成员
        ThreadLocalMap map = getMap(t);
        // 成员不为null
        if (map != null) {
            // 获取map中的entry对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 对象不为null,返回值
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 初始化成员,并赋值为null
        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;
    }

    protected T initialValue() {
        return null;
    }

从上面可以看到,这是一个类似hashMap的操作,在set的时候通过key存储数据,在get的时候使用key来获取存储的内容,接下来看ThreadLocalMap这个内部类:

    static class ThreadLocalMap {
        // 核心类Entry继承于WeakReference,弱引用ThreadLocal,强引用value
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

        // 类似hashmap,容量为2^4
        private static final int INITIAL_CAPACITY = 16;
        // 以数组的形式保存
        private Entry[] table;
        // 扩容阈值
        private int threshold;

        // 默认构造函数
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            // 初始化table
            table = new Entry[INITIAL_CAPACITY];
            // 根据key的hash值计算坐标值
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            // 以Entry对象保存key和value值
            table[i] = new Entry(firstKey, firstValue);
            // 容量初始化为1
            size = 1;
            // 设置扩容阈值为16*2/3 = 10
            setThreshold(INITIAL_CAPACITY);
        }

        // 类似数组的复制,初始化ThreadLocalMap
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

        private Entry getEntry(ThreadLocal<?> key) {
            // 根据key的hash值计算坐标
            int i = key.threadLocalHashCode & (table.length - 1);
            // 获取table当前坐标的内容
            Entry e = table[i];
            // 如果e不为null且key值相等,返回
            if (e != null && e.get() == key)
                return e;
            else
                // 查找对象
                return getEntryAfterMiss(key, i, e);
        }

        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
            // 如果e不为null,说明key不一样
            while (e != null) {
                // 获取当前key
                ThreadLocal<?> k = e.get();
                // key值相等,返回
                if (k == key)
                    return e;
                // tab[i]位置的key值为null
                if (k == null)
                    // 清除i位置的内容,并对Entry数组进行一个重拍
                    expungeStaleEntry(i);
                else
                    // i右移一位
                    i = nextIndex(i, len);
                // 获取右移一位的内容
                e = tab[i];
            }
            // 没有找到对象,返回null
            return null;
        }

        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            // 消除该位置的内容
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            // 从staleSlot+1开始,tab[i]==null的时候结束
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                // 获取i位置的ThreadLocal
                ThreadLocal<?> k = e.get();
                // 如果为null,则清空该对象
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    // 计算一个坐标值
                    int h = k.threadLocalHashCode & (len - 1);
                    // 如果它的坐标值不等于i,说明它可能是移位到这里来的
                    if (h != i) {
                        // 将该位置清空
                        tab[i] = null;
                        // 如果h位置已经有数据了,那么使用开放寻址法-线性寻址法找到一个空位,并赋值
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

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


        private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            // 根据hash值计算坐标i
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                // 如果key相同,更新value
                if (k == key) {
                    e.value = value;
                    return;
                }
                // 如果当前位置的key为null
                if (k == null) {
                    // 替换过期的 Entry
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            // 若数组下标为 i 的位置为空,将要存储的元素放到 i 的位置
            tab[i] = new Entry(key, value);
            int sz = ++size;
            // 若清理过期的Entry数量为0,且数组的大小达到阈值,执行 rehash 操作
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;
            // 从 staleSlot 开始向前遍历,若遇到过期的槽(Entry 的 key 为空),更新 slotToExpunge
            // 直到 Entry 为空停止遍历
            int slotToExpunge = staleSlot;
            // prevIndex是向左移动一位(-1)
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                // 找到一个空值,并赋值给slotToExpunge
                if (e.get() == null)
                    slotToExpunge = i;
            // 从 staleSlot 开始向后遍历,若遇到与当前 key 相等的 Entry,更新旧值,并将二者换位置
            // 目的是把它放到「应该」在的位置
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                // 如果key相同,更新value,并交换staleSlot与当前位置
                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // 如果左右找到的是同一个值,更新slotToExpunge
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    // 删除或者重新计算
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // 如果反向查找没有找到什么,那么选取第一个
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // 若未找到 key,说明 Entry 此前并不存在,新增
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // 删除之前的内容
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }

从上面可以看到,ThreadLocalMap用数组存储Entry数据,使用线性寻址法来应对hash冲突。

ThreadLocal引发的内存泄漏

ThreadLocal通常是用作成员变量或者静态变量来使用,存放在堆中,如果是使用new Thread()来创建线程,Thread会持有一个ThreadLocalMap的强引用,同时ThreadLocalMap中的Entry会持有ThreadLocal的一个弱引用,在线程执行完毕的时候,线程结束,栈内存被回收,Thread对ThreadLocalMap的引用断开,GC的时候,弱引用也会断开,整个ThreadLocalMap都会被回收。

然而在使用线程池的时候,线程是一直存活的,GC的时候,Entry对ThreadLocal的弱引用会断开,Entry的key为null,但value不为null,此时如果不对ThreadLocalMap进行清理,那么Entry中的value对象就会发生内存泄漏。解决方法也很简单,在线程运行完的时候执行remove即可。

参考文献

ThreadLocal到底有没有内存泄漏?从源码角度来剖析一波

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