Android 线程第二弹——ThreadLocal

713 阅读6分钟

前言

最近看源码的时候,多次看到了 ThreadLocal 这个类,但是感觉自己在开发过程中,基本没怎么用到过这玩意,对它的了解也仅限于是用来做数据隔离的,要注意内存泄露。既然如此,来都来了,就深入分析一下 ThreadLocal 吧。

1. 先说说 ThreadLocal 是啥呗,哪里的源码用到了?

简单来说,ThreadLocal 就是一个数据存储类,可以利用它获取指定线程中的数据,其他线程获取不到,所以实现了数据隔离。

至于源码中的使用场景,Handler 都知道吧,每个线程都需要有自己的 Looper,这时候用 ThreadLocal 就可以轻松实现获取不同线程的 Looper 。所以你可以在 Looper 的源码中看到:

// Looper.java
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

2. 看源码之前,先简单看看 ThreadLocal 是怎么使用的?

为了方便理解,举一个简单的使用示例:

private ThreadLocal<Boolean> threadLocal = new ThreadLocal<>();
    
    private void init() {
        threadLocal.set(false);
        // true
        new Thread("Thread1") {
            @Override
            public void run() {
                threadLocal.set(true);
                Log.d(TAG, threadLocal.get().toString());
            }
        };
        // null
        new Thread("Thread2") {
            @Override
            public void run() {
                Log.d(TAG, threadLocal.get().toString());
            }
        };
    }

3. 接下来该看看 ThreadLocal 的底层原理了吧?

根据上面的使用示例,我们从最简单的 set 方法看起:

// ThreadLocal.java
public void set(T value) {
    Thread t = Thread.currentThread();
    // 获取 map
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

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

这么一看很简单吧,不就是往 map 里面塞数据吗,只不过这个 ThreadLocalMap 是根据线程获取的。那 get() 方法也是一样喽,从 map 里面取数据就完事了。

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;
        }
    }
    return setInitialValue();
}

4. 既然原理就是从 map 里面存或取数据,那就看看这个 ThreadLocalMap 吧?

前面说过,ThreadLocalMap 是根据线程获取的,因为不管是 get 还是 set 都有这行代码:

// ThreadLocal.java
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);

那么 getMap() 中做了什么呢?

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

看来这个 map 其实就是存放在 Thread 类中的 threadLocals 变量。

// Thread.java
ThreadLocal.ThreadLocalMap threadLocals = null;

这下子明白了吧?ThreadLocal 之所以能做到线程之间的数据隔离,是因为在每个线程中都维护了一个 ThreadLocalMap 来存放数据,这样通过 ThreadLocal get 或 set 的数据都只和该线程有关系,才做到了数据隔离。

5. 明白了原理,想不想看看 ThreadLocalMap 是怎么实现的?是继承了 HashMap 吗?

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    private static final int INITIAL_CAPACITY = 16;
    private Entry[] table;
    ...
}

很显然 ThreadLocalMapHashMap 啥关系都没有,所以 HashMap 的特性它也都没有。

ThreadLocalMap 初始化一个大小为 16 的 Entry 数组。每个 Entry 对象中保存了一个 key-value 键值对, key 是 ThreadLocal 对象。

6. 为什么要把 ThreadLocalMap 的 key 设置成弱引用?

不知道你有没有观察到,ThreadLocalMap 中的 Entry 继承了 WeakReference ,在构造函数中会将 ThreadLocal 对象,也就是 key,通过 super() 构造成弱引用。所以 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用。

弱引用的特点就是在下一次垃圾回收的时候一定会被清理掉。 显然,把 key 设置为弱引用,明显是为了防止内存泄漏。举个例子,有 3 个线程同时依赖一个 ThreadLocal 对象获取数据,此时线程 1 运行完了,可以释放内存了,但是由于 ThreadLocal 对象还在被其他线程持有,就导致 ThreadLocalMap 的 Entry 也释放不了,这不就是出现了内存泄漏吗。

所以,通过把 key 设置为弱引用,在下一次 GC 时,就会将 ThreadLocal 也就是 ThreadLocalMap 的 key 清理掉。

7. 弱引用只是对于 key 来说的,GC 是回收了 key,那 value 咋办?

这个问题确实存在,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候都会被清理掉的。使用 ThreadLocal 的线程一直持续运行,那么这个 Entry 对象中的 value 就有可能一直得不到回收,会出现 key 为 null 的 value,发生内存泄露。

我们能想到的问题,写源码的肯定也能想到。其实在执行 ThreadLocalset、remove、rehash 等方法时,它都会扫描 key 为 null 的 Entry,如果发现某个 Entry 的 key 为 null,则代表它所对应的 value 也没有作用了,所以它就会把对应的 value 置为 null。 这样,value 对象就可以被正常回收了。

所以为了避免我们写的代码里面出现内存泄漏,最好在使用完 ThreadLocal 后手动调用 remove() 方法。

8. value 为啥不也设置成弱引用的?这样就不用担心内存泄露了。

当我们使用 threadLocal.set(value) 设置键值对时, value 是可能不存在任何引用的(比如 value = 1),如果 value 设置成了弱引用,那么 GC 时直接就能被回收。 这样就可能会出现 threadLocal.get() 时,获取到的 value 是 null 的。这不就出现问题了吗,所以 value 是不能设置成弱引用的。

key 可以设置成弱引用,是因为 ThreadLocal 本身是 map 的 key。我们使用 ThreadLocal 时,是 new 出来的对象,是一个强引用:ThreadLocal t = new ThreadLocal(); ,当 t 还在使用时,key 也不会被清理。

当线程结束时 t 的强引用没了,ThreadLocalMap 的 key 虽然是我们 new 的 t 对象,但是此时 key 是弱引用呀,GC 后就清理了,所以给 key 设置弱引用才是没毛病的。

9. 既然 ThreadLocalMap 的 key 是 ThreadLocal 对象,那发生了 hash 冲突时怎么办?

的确,ThreadLocalMap 也没有继承自 HashMap,所以它肯定是有自己的解决哈希冲突的方式,所以看一下 ThreadLocalMapset() 方法,看看如何解决哈希冲突:

private void set(ThreadLocal<?> key, Object value) {
    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) {
            // 直接刷新 value
            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();
}

HashMap 通过「链地址法」不同的是,ThreadLocalMap 是通过 「线性探测法/开放地址法」 来解决哈希冲突。从上面的源码也能看出来,ThreadLocalMap 在存储时会给每一个 key 计算一个 hash 值,在插入过程中根据 hash 值计算位置 i,如果当前位置是空的,就可以初始化一个 Entry 对象放到位置 i 了。不为空并且是同一个 key 就刷新 value。如果不是同一个 key,那就找下一个空位置,直到为空为止。

ThreadLocalMap 之所以用线性探测来解决哈希冲突,是因为它和 HashMap 的作用定位是不一样的。HashMap 主要是要保证性能,能做到 O(1) 的访问数据。但是对于 ThreadLocalMap 来说,使用线性探测法,可以在扫描节点时,主动发现过期数据并清理掉,降低内存泄漏发生的概率。这点对于 ThreadLocalMap 来说相比于性能是更重要的。

参考

blog.csdn.net/qq_39552268…

www.cnblogs.com/aobing/p/13…