ThreadLocal 工作原理

82 阅读3分钟

1. ThreadLocal 简介

ThreadLocal 是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,并且这些数据只能在相应的线程中获取到。在日常开发中,虽然 ThreadLocal 使用场景较少,但在某些特定场景下,ThreadLocal 能简化一些复杂功能的实现。例如,在 LooperActivityThreadAMS(Activity Manager Service) 中都用到了 ThreadLocal

通常,当数据是以线程为作用域,并且不同线程需要不同的数据副本时,就可以考虑使用 ThreadLocal。例如,对于 Handler 来说,它需要获取当前线程的 Looper,而 Looper 是一个以线程为作用域的类,不同线程拥有不同的 Looper

2. ThreadLocal 基本用法

首先定义一个 ThreadLocal 对象,这里选择 Boolean 类型,如下:

private ThreadLocal<Boolean> mThreadLocal = new ThreadLocal<>();

然后分别在主线程、子线程 1 和子线程 2 中设置和访问它的值,代码如下:

private static void test() {
    mThreadLocal.set(true);
    Log.d(TAG, "[Thread#main] mThreadLocal=" + mThreadLocal.get());

    new Thread("Thread#1") {
        @Override
        public void run() {
            mThreadLocal.set(false);
            Log.d(TAG, "[Thread#1] mThreadLocal=" + mThreadLocal.get());
        }
    }.start();

    new Thread("Thread#2") {
        @Override
        public void run() {
            Log.d(TAG, "[Thread#2] mThreadLocal=" + mThreadLocal.get());
        }
    }.start();
}

在上述代码中,主线程设置了 mThreadLocal 的值为 true,子线程 1 设置了 mThreadLocal 的值为 false,而子线程 2 中没有设置 mThreadLocal 的值,并在三个线程中通过 get 方法获取 mThreadLocal 的值。主线程中值为 true,子线程 1 中值为 false,而子线程 2 由于没有设置值,值应为 null

运行程序,日志如下:

D/MainActivity(946): [Thread#main] mThreadLocal=true
D/MainActivity(946): [Thread#1] mThreadLocal=false
D/MainActivity(946): [Thread#2] mThreadLocal=null

从上面的日志可以看出,虽然在不同线程中访问的是同一个 ThreadLocal 对象,但每个线程中访问到的值都不一样。这是因为每个线程都有自己的 ThreadLocalMap,而 ThreadLocalMap 是一个存储线程局部变量的结构。ThreadLocal 并不直接存储变量的值,而是通过当前线程的 ThreadLocalMap 来存储和获取变量的值。

3. ThreadLocal 的工作原理

ThreadLocal 类结构

ThreadLocal 类的核心方法包括:

  • get()
  • set(T value)
  • remove()
  • initialValue()

ThreadLocal 通过 Thread 类中的一个内部类 ThreadLocalMap 来实现线程局部变量存储。每个线程对象中都有一个 ThreadLocalMap 实例,用于存储线程局部变量。

ThreadLocalMap 结构

ThreadLocalMap 是一个自定义的哈希表,键是 ThreadLocal 对象,值是线程局部变量。ThreadLocalMap 的设计使用了弱引用的键,以便当 ThreadLocal 实例不再被引用时,可以被垃圾回收,从而避免内存泄漏。

以下是 ThreadLocalMap 的简化代码结构:

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

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

    private Entry[] table;
    private int size = 0;

    // 省略构造方法及其他实现
}

get() 方法

当调用 get 方法时,ThreadLocal 通过当前线程的 ThreadLocalMap 获取线程局部变量的值:

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

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;
}

get 方法首先获取当前线程的 ThreadLocalMap,然后从中查找当前 ThreadLocal 的值。如果找不到,则调用 setInitialValue 初始化一个值。

set() 方法

set 方法用于设置线程局部变量的值:

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

set 方法同样通过当前线程的 ThreadLocalMap 设置变量值,如果 ThreadLocalMap 不存在,则创建一个新的 ThreadLocalMap

remove() 方法

remove 方法用于移除当前线程的局部变量:

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

4. 内存泄漏问题

尽管 ThreadLocal 通过弱引用机制减少了内存泄漏的可能,但不正确的使用仍然可能导致内存泄漏。例如,如果在线程池中使用 ThreadLocal,没有显式调用 remove 方法来清理数据,线程对象可能会长期存在,导致内存泄漏。因此,在使用 ThreadLocal 时,务必在不再需要时调用 remove 方法清理数据。