那些年背过的题:ThreadLocal设计与实现

220 阅读4分钟

ThreadLocal 是 Java 中用于实现线程本地存储的类。如果ThreadLocal 变量是静态的(通常这样定义),则该 ThreadLocal 实例在类加载时只会被创建一次。ThreadLocal会存储每个线程的独立变量副本,这样可以避免多线程环境下的并发问题。

比较常用的场景:存储请求的上下文信息(比如用户信息),之后在代码调用链路中使用到时,通过上下文直接获取,减少数据之间的传递依赖性。

1. 基本概念

ThreadLocal 提供了线程局部变量。每个访问该变量的线程都拥有该变量的独立副本,互不干扰。它是一种非常简单且强大的机制,用于在多线程环境中保持数据的一致性和隔离性。

2. 使用方法

2.1 创建 ThreadLocal 变量

你可以使用 ThreadLocal 类来创建静态线程局部变量:

public class ThreadLocalExample {
//`ThreadLocal` 变量是静态的(通常这样定义),则该 `ThreadLocal` 实例在类加载时只会被创建一次
    private static final ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 0);

    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable(), "Thread-1");
        Thread t2 = new Thread(new MyRunnable(), "Thread-2");

        t1.start();
        t2.start();
    }

    public static class MyRunnable implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                int currentValue = threadLocalValue.get();
                threadLocalValue.set(currentValue + 1);
                System.out.println(Thread.currentThread().getName() + " - " + threadLocalValue.get());
            }
            // 清理,以防内存泄漏
            threadLocalValue.remove();
        }
    }
}

在这个例子中,每个线程都有自己独立的 threadLocalValue 副本,所以即使多个线程修改这个值,也不会相互影响。

2.2 方法介绍

  • get() : 返回当前线程中的此线程局部变量的值。
  • set(T value) : 将此线程局部变量的当前线程副本中的值设置为指定值。
  • remove() : 移除当前线程中此线程局部变量的值,可以避免潜在的内存泄漏问题;

3. 实现原理

ThreadLocal 的实现依赖于内部维护的一个 ThreadLocalMap,map存储所有访问线程的局部变量副本。

ThreadLocal 的核心是通过 ThreadLocalMap 来保存和获取每个线程的局部变量值。

public class ThreadLocal<T> {
    static class ThreadLocalMap {
        // Entry 是继承自 WeakReference 的静态内部类
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        private Entry[] table;
        
        // 实现省略...
    }
    
    protected T initialValue() {
        return null;
    }

    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;
    }
    
    // 获取当前线程的 ThreadLocalMap
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

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

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

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

4. 注意事项

  1. 内存泄漏

    • 因为 ThreadLocalMap 的键是弱引用,即使 ThreadLocal 对象被 GC 回收,其对应的值仍然可能存在于内存中,导致内存泄漏。
    • 为了避免内存泄漏,在使用完 ThreadLocal 后,应调用 remove() 方法来显式移除值。
  2. 适用场景

    • ThreadLocal 适用于每个线程需要自己独立实例的场景,例如用户会话信息、数据库连接等。
    • 不适用于共享资源的情况,因为它的设计初衷就是为了隔离线程间的数据。
  3. 性能考虑

    • 虽然 ThreadLocal 提供了一种方便的方式来处理线程本地变量,但频繁创建和销毁局部变量可能会带来一定的性能开销。因此,在性能敏感的应用中,需要权衡其使用。

5. 思考题

5.1 为什么使用弱引用?

ThreadLocal 的键是弱引用,这样设计的原因是为了允许垃圾回收器在没有外部引用时可以回收 ThreadLocal 实例,从而避免内存泄漏。但这同时也意味着需要开发者显式地清理不再使用的 ThreadLocal 值。

5.2 如果存在线程池调用ThreadLocal,会存在什么问题

  • 线程复用:线程池中的线程通常会被重复使用,而不会在每次任务完成后销毁。这意味着同一个线程可能会处理多个不同的任务。
  • 如果某个线程在执行完一个任务后,其 ThreadLocal 变量没有清理,那么该变量及其值会一直存在于该线程中,当这个线程被线程池复用去执行另一个任务时,遗留的数据仍然存在,导致读取到之前的数据(比如用户B请求接口,读取到用户A的数据)。