ThreadLocal源码分析和相关理解

总结

总结放前面防止太长不看:

  1. 每个线程都有一个threadLocals字段,是一个ThreadLocalMap的实例,所有的ThreadLocal代表的线程私有数据都存放在这里面,key对应的是一个ThreadLocal对象,而value就是线程私有的值。
    • ThreadLocalMap对象的创建是懒惰的,第一次在该线程上用到ThreadLocal时才会创建,初始容量是16,threshold默认为2/3的数组大小,如果元素个数超过threshold,清理一次无用的Entry(key被回收掉的Entry),如果清理一次之后元素个数仍大于3/4*threshold,进行扩容(翻倍)
    • ThreadLocalMap通过线性探测法解决冲突。
    • ThreadLocalMap通过弱引用指向一个ThreadLocal对象(key),强引用指向该线程对应的值(value)
  2. key->ThreadLocal对象是弱引用,代表ThreadLocal对象一旦只剩下线程中ThreadLocalMap中key的引用,下一次GC的时候就会被回收。但该Entry和内部的value仍会占用空间,ThreadLocalMap中的许多方法都在操作前后检查了数组内部是否有无用的Entry,如果有就清除掉,算是防止内存泄漏的一个措施。

ThreadLocal概述

为共享变量在每个线程中创建一个副本,每个线程可以访问自己内部的副本变量

private static Integer num = 0;

private static ThreadLocal<Integer> numLocal = new ThreadLocal<Integer>(){
    @Override
    protected Integer initialValue() {
        return 0;
    }
};

public static void main(String[] args) {
    Thread[] threads = new Thread[5];
    for (int i = 0;i<threads.length;i++){
        threads[i] = new Thread(()->{
            int x = numLocal.get().intValue();
            x+=5;
            numLocal.set(x);
            System.out.println(Thread.currentThread().getName()+":"+numLocal.get());
        },"Thread-"+i);
    }
    for (int i = 0;i<threads.length;i++){
        threads[i].start();
    }
}

重要方法

set

public void set(T value) {
    Thread t = Thread.currentThread();
    //通过当前线程获取一个ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    //如果该对象不为空调用set方法,可以猜测类似map接口的set
    if (map != null)
        map.set(this, value);
    //如果该对象是空则调用createMap方法,可以猜测能创建一个ThreadLocalMap对象
    else
        createMap(t, value);
}

ThreadLocalMap

先来看这个叫做ThreadLocalMap的内部类:

static class ThreadLocalMap {
    /**
    * Entry采用弱引用指向一个ThreadLocal对象,这个ThreadLocal始终
    * 是这个ThreadLocalMap的键(key)
    */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** 这个值代表ThreadLocal关联的值. */
        Object value;

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


    //初始容量,容量必须是2的整数次幂
    private static final int INITIAL_CAPACITY = 16;

    //存放数据的Entry数组,类似于HashMap的Node数组
    private Entry[] table;

    //Entry的个数
    private int size = 0;

    //类似于HashMap的threshold,size大于这个值则扩容
    private int threshold; // Default to 0
    //...
}

从这里可以看出,ThreadLocalMap和HashMap很大的区别是,Entry没有next属性,至少说明不会以链表形式存储。

我们通过getMap()方法发现,一个Thread对象的内部有一个threadLocals字段,是一个ThreadLocalMap的对象,并且线程创建并不会给这个字段初始化。

那么我们假设第一次在一个线程上调用set方法,此时map==null,调用createMap()创建一个ThreadLocalMap对象:

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

可以看到这个方法还顺便把这次要添加的数据添加了进去。

我们返回来看ThreadLocalMap的set方法,看看是否和HashMap类似:

ThreadLocalMap.set

private void set(ThreadLocal<?> key, Object value) {

    Entry[] tab = table;
    int len = tab.length;
    // 非常类似HashMap的数组下标计算方法,下文我们将一个数组位置称为slot
    int i = key.threadLocalHashCode & (len-1);

    // 可以发现,这段代码是用线性探测法解决slot冲突
    for (Entry e = tab[i];
            e != null;
            e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }
        // k什么时候为null?因为k是被弱引用的,如果k为null说明中途被回收了,则清理该slot,并把数据放进去
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    //先检查i后面的slot是否有被回收的引用,如果有则清理,否则size大于threshold则扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

ThreadLocalMap.cleanSomeSlots

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];
        //检查是否有被回收的引用
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            // 清理该slot
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}

ThreadLocalMap.expungeStaleEntry

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;
    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 {
            // 对于还没有被回收的情况,需要做一次 rehash。
            //如果对应的 ThreadLocal 的 ID 对 len 取模出来的索引 h 不为当前位置 i,
            //则从 h 向后线性探测到第一个空的 slot,把当前的 entry 给挪过去。
            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;
}

ThreadLocalMap.rehash

private void rehash() {
    expungeStaleEntries();

    // 如果清理完所有的slot,size仍然大于3/4的threshold,立即扩容防止滞后现象
    if (size >= threshold - threshold / 4)
        resize();
}

threadLocalHashCode

再来看看这个threadLocalHashCode是怎么计算的:

private final int threadLocalHashCode = nextHashCode();

private static AtomicInteger nextHashCode =
    new AtomicInteger();

private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

每个 ThreadLocal 对象都有一个 hash 值 threadLocalHashCode,每初始化一个 ThreadLocal对象,hash 值就增加一个固定的大小 0x61c88647

get

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //getEntry来查找是否有这个元素
        ThreadLocalMap.Entry e = map.getEntry(this);
        //如果map中存在该值,返回
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //否则setInitialValue,并将结果返回
    return setInitialValue();
}

getEntry

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        //由于采用线性探测法,如果没有找到需要继续向后查找
        return getEntryAfterMiss(key, i, e);
}

setInitialValue

private T setInitialValue() {
    //从initialValue方法获取初始值
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    //set到map中
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

而initialValue默认返回null,我们可以在创建ThreadLocal对象时重写initialValue方法来指定第一次调用get获取的值。

remove

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        //直接调用ThreadLocalMap的remove方法移除
        m.remove(this);
}

ThreadLocalMap.remove

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) {
            //在Entry上调用clear
            e.clear();
            //清理后面的slot
            expungeStaleEntry(i);
            return;
        }
    }
}

问题

为什么 ThreadLocalMap 的 Key 是弱引用

如果是强引用,ThreadLocal 将无法被释放内存。

因为如果这里使用普通的 key-value 形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在 GC 分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。弱引用是 Java 中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次 GC。当某个ThreadLocal 已经没有强引用可达,则随着它被垃圾回收,在 ThreadLocalMap 里对应的 Entry的键值会失效,这为 ThreadLocalMap 本身的垃圾清理提供了便利。

听说 ThreadLocalMap 还是会造成内存泄漏?

发生内存泄漏的情况:

  1. ThreadLocal还被强引用着,但线程已经不需要这份私有数据了,如果不remove掉会一直保留,造成内存泄漏。
  2. ThreadLocal已经没有强引用了,线程中的ThreadLocalMap中一个Entry的key引用的ThreadLocal对象也被垃圾回收掉了,但该Entry和value值仍会占用空间,不过ThreadLocal会经常检查key为空的Entry并清除,所以该情况可以忽视。
  3. 在线程池中使用ThreadLocal可能导致内存泄露,因为线程是会一直存在并被复用的,如果不需要这份数据了,需要及时调用remove方法进行清理。

这三种情况总而言之归纳成一句话,不需要这份数据了一定要及时调用remove方法进行清理。

原创文章,作者:彭晨涛,如若转载,请注明出处:https://www.codetool.top/article/threadlocal%e6%ba%90%e7%a0%81%e5%88%86%e6%9e%90%e5%92%8c%e7%9b%b8%e5%85%b3%e7%90%86%e8%a7%a3/

发表评论

电子邮件地址不会被公开。