JUC并发编程(四):ThreadLocal 源码解析

55 阅读3分钟

前言

在日常开发中,有时候用ThreadLocal 来传递上下文或者某些变量是非常方便的,因为传递的对象是绑定在线程上的。那么ThreadLocal是如何实现的呢? 以及使用过程中有哪些注意事项? 我们跟着源码来一起看一下

原理

上面说道传递的对象是绑定在线程上的,这是如何实现的呢?

在Thread 类中有一个成员变量 threadLocalMap

ThreadLocal.ThreadLocalMap threadLocals = null;

//ThreadLocalMap 定义
static class ThreadLocalMap {

    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

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

可以清晰地看到,每个线程对象有一个 threadLocalMap ,这个threadLocalMap 是一个 (k,v)的存储形式,其中 k 就是当前线程对象, v 就是我们存储的对象。

下面我们看下 ThreadLocalset方法来进行验证

set 方法

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

根据当前线程 t 获取对应的 threadLocalMap
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

//threadLocalMap 中的设值方法
private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    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)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

上述set 方法中,需要留一下这段代码 ,后面我们会再提到这一块

if (k == null) {
        replaceStaleEntry(key, value, i);
        return;
  }

get 方法

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;
        }
    }
    //没有map的话 ,则进行初始化
    return setInitialValue();
}

// 设置初始值 null 
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;
}

看完上述两个方法,大体上这个对象也就清晰了
调用 ThreadLocal.set(对象A) 方法时,实际上就是在当前线程对象上的属性 threadLocalMap 上做一个set(当前threadLocal指针, 对象A)的操作

内存泄漏的情况和原因

泄漏案例

看一段代码

public class ThreadLocalTest {
    public static void main(String[] args) {
        ThreadLocal<Room> local = new ThreadLocal<>();
        local.set(new Room());
        local = null;
        System.out.println(local);
    }
}

相信很多人都知道,这种情况是有内存泄漏的,也就是执行GC后,threadLocalMap里的对象没有被回收。
在 local = null 这一行打断点,打开 java Visualvm观察到这一行前后(进行GC)对象数量没有变化。

image.png local = null 还未执行的对象数量

image.png local = null 已经执行过,并且进行了GC

未泄漏的案例

下面这一段不会发生内存泄漏的代码

public class ThreadLocalTest {
    public static void main(String[] args) {
        ThreadLocal<Room> local = new ThreadLocal<>();
        local.set(new Room());
        local.remove();
        local = null;

        System.out.println(local);
    }
}

image.png local = null 还未执行的对象数量 其中 entry 18个 ,threadLocalMap 4个

image.png local = null 已经执行过,并且进行了GC

很明显,通过上述两段代码,我们验证了 threadLocal 内存泄漏的情况,因此每次使用完后需要手动执行 remove 方法。

为什么会泄漏

我们跟着代码来讲一下为什么第一段会内存泄漏

ThreadLocal<Room> local = new ThreadLocal<>();
local.set(new Room());

在这里的时候,实例化了一个 new ThreadLocal 对象 并指向了 local ,同时还实例化了一个 Room对象;

local = null;

当我们执行 local = null 时 ,断开了 local 与 new ThreadLocal 对象之间的联系。
按道理来说,new ThreadLocal 这个对象应该要被GC回收。
但是这个对象中的 threadLocalMap 中有一个entry。这个entry的key 是当前threadLocal的指针 ,value 是 room 对象。
也就是说只要当前线程还存活,这个entry就是有引用的,因此这个 new ThreadLocal 也是不会被回收,也就发生了内存泄漏的情况。

弱引用的好处

还记得我们上面说的 ,其中 k ==null 那一部分的代码吗

//threadLocalMap 中的设值方法
private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    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)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }
        // k 为 null 时,清除掉对应的value
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

也就是同一个线程,第二次做set操作时是清除掉之前 k = null (防止对应的threadlocal指针被置空)对应的value值 。

为什么要用弱引用呢? 准确地说是 entry的 key 为弱引用 。

public class ThreadLocalTest {
    public static void main(String[] args) {
        test2();
        System.out.println(111);
    }

    public static void test2() {
        ThreadLocal<Room> local = new ThreadLocal<>();
        local.set(new Room());

        System.out.println(local);
    }
}

在test2 方法执行完,但 main 方法未执行完时。
因此 test2执行完,栈帧都销毁了, local的强引用也就没了,此时有一个 threadLocal 对象里还有个 threadLocalMap。

image.png

此时我们发现,threadLocal对象只会被 Entry引用。如果entry 的key 是个强引用,那么threadLocal就不可回收了;但是如果是个弱引用,那么这个key就会被回收,entry就会变成 (null , value) 。

实际上我们翻看remove方法就会发现,remove的时候是同时把key value都置为 null 了。