问题

什么是ThreadLocal,如何实现的?

答案

核心概念

ThreadLocal(线程局部变量)是一种线程隔离机制,为每个使用该变量的线程提供独立的变量副本,各线程之间互不干扰。

核心特性

  • 每个线程都有自己独立的变量副本
  • 线程之间数据隔离,无需同步
  • 线程结束时,其ThreadLocal变量会被自动回收

实现原理

核心设计思想

  • 变量不是存储在ThreadLocal对象中,而是存储在每个Thread对象内部的ThreadLocalMap
  • ThreadLocal充当”key”的角色,通过它在当前线程的Map中存取值

关键数据结构

public class Thread {
    // 每个线程都有一个ThreadLocalMap实例
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

public class ThreadLocal<T> {
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = t.threadLocals;  // 获取当前线程的Map
        if (map != null)
            map.set(this, value);  // this作为key,存储value
        else
            createMap(t, value);   // 首次使用时创建Map
    }

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = t.threadLocals;
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);  // 以this为key查找
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();  // 未设置时返回初始值(默认null)
    }

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

ThreadLocalMap内部结构

Entry结构

static class ThreadLocalMap {
    // Entry继承WeakReference,key是ThreadLocal的弱引用
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);  // key为弱引用
            value = v; // value为强引用
        }
    }

    private Entry[] table;  // 使用数组存储,非链表
    private int size;
    private int threshold;  // 扩容阈值(capacity * 2/3)

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

哈希计算

// ThreadLocal使用自增ID作为hashCode
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);
}

// 计算索引位置
int i = key.threadLocalHashCode & (len - 1);

魔数0x61c88647的作用

  • 斐波那契散列,能够在2的幂次容量下均匀分布,减少哈希冲突
  • 即使连续创建多个ThreadLocal,也能避免聚集

冲突解决:线性探测法

ThreadLocalMap使用开放地址法(线性探测)而非链表法:

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len - 1);  // 计算初始位置

    // 线性探测:向后查找空位或匹配的key
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;  // 找到相同key,更新value
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);  // key已被GC,替换
            return;
        }
    }

    tab[i] = new Entry(key, value);  // 找到空位,插入
    size++;

    if (!cleanSomeSlots(i, size) && size >= threshold)
        rehash();  // 清理后仍超过阈值,则扩容
}

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);  // 循环递增
}

为什么使用弱引用

Entry的key为何设计为WeakReference

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

原因

  • 如果key是强引用,当外部ThreadLocal对象置为null时,Thread对象仍持有强引用,导致ThreadLocal无法被GC
  • 使用弱引用后,即使Thread存活,ThreadLocal对象也能在下次GC时被回收

流程图

外部ThreadLocal引用 --> ThreadLocal对象 <-- WeakReference(key)
                                              ↑
                                          ThreadLocalMap.Entry
                                              ↓
                                            value

当外部ThreadLocal引用置为null后:

  • 下次GC时,ThreadLocal对象被回收
  • Entry的key变为null(弱引用特性)
  • ThreadLocalMap在set/get/remove时会清理key为null的Entry

完整执行流程示例

public class ThreadLocalDemo {
    // 1. 创建ThreadLocal(静态变量,所有线程共享同一个ThreadLocal对象)
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        // 主线程设置值
        threadLocal.set("主线程Value");
        System.out.println("主线程读取: " + threadLocal.get());

        // 子线程1
        new Thread(() -> {
            threadLocal.set("子线程1Value");
            System.out.println("子线程1读取: " + threadLocal.get());
            threadLocal.remove();  // 及时清理,避免内存泄漏
        }).start();

        // 子线程2
        new Thread(() -> {
            System.out.println("子线程2读取: " + threadLocal.get());  // null
        }).start();
    }
}

// 输出:
// 主线程读取: 主线程Value
// 子线程1读取: 子线程1Value
// 子线程2读取: null

执行过程

  1. 主线程执行threadLocal.set("主线程Value")
    • 获取主线程的Thread对象
    • 创建主线程的ThreadLocalMap(首次使用)
    • 将Entry(threadLocal, “主线程Value”)存入Map
  2. 子线程1执行threadLocal.set("子线程1Value")
    • 获取子线程1的Thread对象
    • 创建子线程1的ThreadLocalMap
    • 存储独立的值,不影响主线程
  3. 子线程2执行threadLocal.get()
    • 未设置过值,返回null(或initialValue)

关键源码解析

初始值设置

public class ThreadLocal<T> {
    protected T initialValue() {
        return null;  // 默认返回null
    }

    // JDK8新增:Lambda方式设置初始值
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }
}

// 使用示例
ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(
    () -> new SimpleDateFormat("yyyy-MM-dd")
);

面试答题要点

  1. 设计原理:变量存储在Thread对象的ThreadLocalMap中,ThreadLocal只是作为key
  2. 线程隔离:每个线程有独立的ThreadLocalMap,实现数据隔离,无需同步
  3. 底层结构:ThreadLocalMap使用Entry数组,通过线性探测法解决冲突
  4. 弱引用设计:Entry的key为WeakReference,防止ThreadLocal对象无法回收
  5. 使用规范:必须调用remove()清理,避免内存泄漏(尤其在线程池场景)
  6. 适用场景:数据库连接、用户会话、SimpleDateFormat等需要线程隔离的对象