ThreadLocal简析

1. 属性

ThreadLocal有三个属性threadLocalHashCodenextHashCodeHASH_INCREMENT

  • threadLocalHashCode属于对象的,每个ThreadLocal对象hashcode在初始化时确定且不可变
  • nextHashCode属性是静态的随着ThreadLocal类的加载而加载,分配一个AtomicInteger对象,用来以原子的方式获取最新的hashcode
  • HASH_INCREMENT是下一个hashcode增长数,是一个静态常量
public class ThreadLocal {
    // 每个ThreadLocal实例的hashcode(在对象被创建时赋值)
    private final int threadLocalHashCode = nextHashCode();
	// 下一个要给出的hashcode
    private static AtomicInteger nextHashCode = new AtomicInteger();
    // hash增长数
    private static final int HASH_INCREMENT = 0x61c88647;
    
    // 返回下一个hash码
    private static int nextHashCode() {
        // 原子的方法更新值(调用unsafe操作)
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
}

2. 常用方法

2.1 get()

用来获取线程的私有变量,操作步骤如下:

  1. 获取当前线程的threadLocalMap属性,若初始化完成则进入步骤2,否则进行初始化

  2. 通过threadLocalthreadLocalMap获取对应entry,若无entiry则进行初始化

public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程中threadLocalMap
    ThreadLocalMap map = getMap(t);
    // 判断threadLocalMap有没有被初始化
    if (map != null) {
        // 获取entity
        ThreadLocalMap.Entry e = map.getEntry(this);
        // entity为null(可能发生内存泄漏,所以设置初始值)
        // 此时的引用链为Thread->ThreadLocalMap->Entry(null)->value
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 初始化
    return setInitialValue();
}

// 获取线程t的threadLocalMap
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// 设置初始值
private T setInitialValue() {
    // 初始值(null)
    T value = initialValue();
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 放入map
        map.set(this, value);
    else
		// 创建map
        createMap(t, value);
    return value;
}
// 初始化value
protected T initialValue() {
    return null;
}
2.2 set()

set方法用来设置value到线程的threadLocalMap

public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
2.3 remove()

移除ThreadLocalmap中的value

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

3. 内部类ThreadLocalMap

ThreadLocalMap的结构如下图

ThreadLocal简析_第1张图片

3.1 为什么Entry要继承弱引用?

Entry实现了对Key(也就是ThreadLocal)的弱引用。如果使用强引用,只要线程没有被销毁,ThreadLocal就一直是引用可达状态,永远无法被回收,程序不可知ThreadLocal是否可被清理。如果使用弱引用,当没有强引用链可达时,则活不过下一个GC,ThreadLocal会被回收

static class ThreadLocalMap {
    // 内部类Entry,实现了弱引用Key
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    
    // 静态常量:Table默认初始值
    private static final int INITIAL_CAPACITY = 16;
	// Entry数组
    private Entry[] table;
	// 初始大小
    private int size = 0;
	// 阈值
    private int threshold; // Default to 0
    
    // 实际上Entry[]数组以一个环的形式存在
    // 获取下一个下标
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }
	// 获取上一个下标
    private static int prevIndex(int i, int len) {
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }
    // 构造方法(懒加载,至少放入一个KV)
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];	// 16
        // 做与运算确定下标,和HashMap确定下标方式一样
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        // 构建entry放入table[i]
        table[i] = new Entry(firstKey, firstValue);
        // 设置ThreadLocalMap大小
        size = 1;
        // 设置阈值
        setThreshold(INITIAL_CAPACITY);
    }
}
3.2 getEntity()方法

获取ThreadLocalMap中的value,步骤如下:

  1. ThreadLocalMap对应下标处的entry存在且entry的key就是传入key,返回value,否则进入2
  2. 至此说明hash冲突entry不存在,如果entry不存在,则返回null,否则进入3
  3. 从e开始线性探测向后查找不为null的entry,若命中则返回value,若发现失效entry则进行连续段清理
// 获取entry,被ThreadLocal的get方法调用
private Entry getEntry(ThreadLocal<?> key) {
    // 与运算获取到key对应下标
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // 下标处entry存在 且 Entry的弱引用key没有失效
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}
    
// 在getEntry()中未命中,使用本方法
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    // 基于线性探测法不断向后探测直至遇到空的Entry
    while (e != null) {
        ThreadLocal<?> k = e.get();
        // 命中
        if (k == key)
            return e;
        if (k == null)
            // 弱引用key失效被回收,清理下标i无效的Entry
            expungeStaleEntry(i); 
        else
            // 线性探测下一个位置
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}
    
// 清除staleSlot开始的陈旧条目(连续段的清理)
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 下标staleSlot的value引用断开,原entry的ThreadLocal已被回收,此时原value对象可被回收
    tab[staleSlot].value = null;
    // 下标staleSlot出entry引用断开
    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) {
            // 对已回收的Entry进行清理操作
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // rehash重新确定位置
            int h = k.threadLocalHashCode & (len - 1);
            /**
             * 重新取模后的h位置与原位置i不相等,
             * 则从h向后线性探测找到第一个空的位置,将tab[i]放入
             */
            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;
            }
        }
    }
    // 返回staleSlot之后第一个空索引
    return i;
}
3.3 set()方法

设置ThreadLocalMap中的kv,步骤如下:

  1. 通过hash拿到下标,下标处为null,直接插入,否则进入2
  2. 下标处开始向后遍历,此时可能遇到三种情况:
    • key值相等,直接替换value
    • key失效,插入到此位置
  3. 是否需要rehash
private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    // 获取key的下标
    int i = key.threadLocalHashCode & (len-1);

    /**
     * hash冲突,下标i位置存在Entry
     * 这时的Entry有两种状态:
     * Entry的ThreadLocal未被回收,若ThreadLocal为k直接放入value
     * Entry的ThreadLocal被回收,替换无效slot
     */
    for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        // 找到对应的Entry
        if (k == key) {
            e.value = value;
            return;
        }

        // Entry的ThreadLocal被回收,直接替换
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // i下标处放入Entry
    tab[i] = new Entry(key, value);
    int sz = ++size;
    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;

    // 向前遍历,找到第一个entry存在但key无效的slot
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

	// 向后遍历tab
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
		
        // 找到对应key,与无效slot交换
        if (k == key) {
            e.value = value;

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

			// 确定清理点
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            // 做一次连续段清理,再做一次启发式清理
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // 当前slot无效且向前扫描没有无效条目,更新slotToExpunge为当前位置
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // 若key在tab中不存在,直接插入
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // 通过slotToExpunge判断是否存在无效条目,若存在,清除
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

/**
 * 启发式清理
 *
 * @param i 永远为一个有效条目,从下一个索引开始判断
 * @param n 扫描log2(n)个单元,除非找到无效slot
 *			插入方法调用时,此参数是元素数量
 * 			replaceStaleEntry方法调用时,此参数是table长度
 * @return 清理过任何无效slot,则返回true
 */
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];
        // e为无效slot
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            // 清理连续段
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    // 删除过任何无效slot,返回true
    return removed;
}

private void rehash() {
    // 做全量清理
    expungeStaleEntries();
    /**
     * 使用较低阈值判断是否需要扩容,上面做了清理,size可能会减小
     * 这里用threshold的3/4来判断
     */
    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)
            // entry为无效slot
            expungeStaleEntry(j);
    }
}

// 将table的容量加倍,对遍历过程中的无效entry直接断开value
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];
        // 判断entry存在
        if (e != null) {
            ThreadLocal<?> k = e.get();
            // 判断key是否有效
            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;
}
3.4 remove()方法

通过key移除entry

  1. 通过hash找到下标开始位置
  2. 向后遍历直至遇到null为止,判断key是否为传入的key,若是清理entry并调用连续段清理
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    // 找到i开始向后查找,找到对应的entry,清理
    for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            // 清理
            expungeStaleEntry(i);
            return;
        }
    }
}

4. ThreadLocal的常见问题?

4.1 ThreadLocal为什么发生内存泄露?

因为ThreadLocalThreadLocalMap的Entry以弱引用的方式做key,当发生GC时,ThreadLocal就会被回收,此时引用链为Thread->ThreadLocalMap->Entry(null)->Value,当线程无法结束(线程池场景,使用完后归还线程池)时,Value将不会被清理,发生内存泄露

解决:使用static修饰ThreadLocal变量,set()get()使用完成之后手动调用remove()方法清除ThreadLocal

参考:ThreadLocal源码解读,散列表–线性探测法,ThreadLocal问题

你可能感兴趣的:(java基础)