Java读源码之ThreadLocal

前言

JDK版本: 1.8java

以前在看Thread源码时候看到这么一个属性面试

ThreadLocal.ThreadLocalMap threadLocals = null;

做用

ThreadLocal实现的是每一个线程都有一个本地的副本,至关于局部变量,这样就能够少一些参数传递,是以空间换时间的一周策略,其实ThreadLocal就是内部本身实现了一个map数据结构。redis

存在的问题

ThreadLocal确实很重要,但想到看源码仍是有个小故事的,以前去美团点评面试,问我如何保存用户登陆token,能够避免层层传递token?数组

心想这好像是在说ThreadLocal,而后开始胡说放在redis里或者搞个ThreadLocal,给本身挖坑了数据结构

面试官继续问,ThreadLocal使用时候主要存在什么问题么?this

完蛋,确实只了解过,没怎么用过,凉凉,回来查了下主要存在的问题以下线程

  • ThreadLocal可能内存泄露?

带着疑惑进入源码吧3d

源码

类声明和重要属性

package java.lang;

public class ThreadLocal<T> {
    
    // hash值,相似于Hashmap,用于计算放在map内部数组的哪一个index上
    private final int threadLocalHashCode = nextHashCode();
    private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT);}
    // 初始0
    private static AtomicInteger nextHashCode = new AtomicInteger();
    // 神奇的值,这个hash值的倍数去计算index,分布会很均匀,总之很6 
    private static final int HASH_INCREMENT = 0x61c88647;
    
    static class ThreadLocalMap {

        // 注意这是一个弱引用
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        // 初始容量16,必定要是2的倍数
        private static final int INITIAL_CAPACITY = 16;
        // map内部数组
        private Entry[] table;
        // 当前储存的数量
        private int size = 0;
        // 扩容指标,计算公式 threshold = 总容量 * 2 / 3,默认初始化以后为10
        private int threshold;

增改操做

让咱们先来看看增改方法code

public void set(T value) {
    Thread t = Thread.currentThread();
    // 拿到当前Thread对象中的threadLocals引用,默认threadLocals值是null 
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 若是ThreadLocalMap已经初始化过,就把当前ThreadLocal实例的引用当key,设置值
        map.set(this, value); //下文详解
    else
        // 若是不存在就建立一个ThreadLocalMap而且提供初始值
        createMap(t, value);
}

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

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

让咱们来看看map.set(this, value)具体怎么操做ThreadLocalMap对象

private void set(ThreadLocal<?> key, Object value) {
    // 获取ThreadLocalMap内部数组
    Entry[] tab = table;
    int len = tab.length;
    // 算出须要放在哪一个桶里
    int i = key.threadLocalHashCode & (len-1);
    // 若是当前桶冲突了,这里没有用拉链法,而是使用开放定指法,index递增直到找到空桶,数据量很小的状况这样效率高
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        // 拿到目前桶中key
        ThreadLocal<?> k = e.get();
        // 若是桶中key和咱们要set的key同样,直接更新值就ok了
        if (k == key) {
            e.value = value;
            return;
        }
        // 桶中key是null,由于是弱引用,可能被回收掉了,这个时候咱们直接占为己有,而且进行cleanSomeSlots,当前key附近局部清理其余key是空的桶
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 若是没冲突直接新建
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 当前key附近局部清理key是空的桶,若是一个也没清除而且当前容量超过阈值了就扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}


private void rehash() {
    // 这个方法会清除全部key为null的桶,清理完后size的大小会变小
    expungeStaleEntries();

    // 此时size还大于阈值的3/4就扩容
    if (size >= threshold - threshold / 4)
        // 2倍扩容
        resize();
}

为何会内存泄漏

总算读玩了set,大概明白了为何会发生内存泄漏,画了个图

ThreadLocalMap.Entry中的key保存了ThreadLocal实例的一个弱引用,若是ThreadLocal实例栈上的引用断了,只要GC一发生,就铁定被回收了,此时Entry的key,就是null,可是呢Entry的value是强引用并且是和Thread实例生命周期绑定的,也就是线程没结束,值就一直不会被回收,因此产生了内存泄漏。

总算明白了,为何一个set操做要这么屡次清理key为null的桶。

既然这么麻烦,为何key必定要用弱引用?

继续看上面的图,若是咱们的Entry中保存的是ThreadLocal实例的一个强引用,咱们删掉了ThreadLocal栈上的引用,同理此时不只value就连key也不会回收了,这内存泄漏就更大了

查询操做

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;
        }
    }
    // 返回null
    return setInitialValue();
}

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 若是只是threadLocals.Entry是空,就设置value为null
        map.set(this, value);
    else
        // 若是threadLocals是空,就new 一个key是当前ThreadLocal,value是空的ThreadLocalMap
        createMap(t, value);
    return value;
}

protected T initialValue() {
    return null;
}

让咱们来看看map.getEntry(this)具体怎么操做ThreadLocalMap

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        // 最好状况,定位到了Entry,而且key匹配
        return e;
    else
        // 多是hash冲突重定址了,也多是key被回收了
        return getEntryAfterMiss(key, i, e);
}

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    // 向后遍历去匹配key,同时清除key为null的桶
    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为null的Entry,是否是咱们就能够放心了,大多数状况是的,可是若是咱们在使用线程池,核心工做线程是不会中止的,会重复利用,这时咱们的Entry中的value就永远不会被回收了这很糟糕,还好源码做者还没给我提供了remove方法,综上所述,养成良好习惯,只要使用完ThreadLocal,必定要进行remove防止内存泄漏

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        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)]) {
        if (e.get() == key) {
            // 主要多了这一步,让this.referent = null,GC会提供特殊处理
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}
相关文章
相关标签/搜索