ThreadLocal 面试题答案

40 阅读22分钟

📊 ThreadLocal 关系图(超简化版)

一、ThreadLocal核心机制

Thread → ThreadLocalMap → Entry(ThreadLocal, Value)

二、线程隔离原理

线程1 → ThreadLocalMap1 → Entry[ThreadLocal1 → "数据1"]
线程2 → ThreadLocalMap2 → Entry[ThreadLocal1 → "数据2"]

三、ThreadLocal在Looper中的应用

Looper.prepare() → ThreadLocal.set(new Looper()) → 绑定到当前线程
Looper.myLooper() → ThreadLocal.get() → 获取当前线程的Looper

2.1 ThreadLocal 基础概念

1. 什么是ThreadLocal?ThreadLocal的作用是什么?

完整答案

ThreadLocal的定义: ThreadLocal是Java提供的线程局部变量,每个线程都有自己独立的变量副本,线程间互不干扰。

ThreadLocal的作用

  1. 线程隔离

    • 每个线程有自己独立的变量副本
    • 线程间数据互不干扰
  2. 避免线程安全问题

    • 不需要使用synchronized
    • 天然线程安全
  3. 简化参数传递

    • 不需要通过方法参数传递
    • 可以在任何地方访问

简单理解: ThreadLocal就像一个"线程专属的储物柜",每个线程都有自己的柜子,互不干扰。

代码示例

// 创建ThreadLocal
ThreadLocal<String> threadLocal = new ThreadLocal<>();

// 线程1
new Thread(() -> {
    threadLocal.set("线程1的数据");
    System.out.println(threadLocal.get());  // 输出:线程1的数据
}).start();

// 线程2
new Thread(() -> {
    threadLocal.set("线程2的数据");
    System.out.println(threadLocal.get());  // 输出:线程2的数据
}).start();

// 每个线程有自己独立的数据,互不干扰

ThreadLocal的核心方法

// 设置值
threadLocal.set("value");

// 获取值
String value = threadLocal.get();

// 移除值
threadLocal.remove();

ThreadLocal在Android中的应用

// 1. Looper中使用ThreadLocal
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

// 2. 存储线程上下文信息
ThreadLocal<Context> contextThreadLocal = new ThreadLocal<>();

总结: ThreadLocal提供线程局部变量,每个线程有独立的变量副本,实现线程隔离和简化参数传递。

2. ThreadLocal和普通变量的区别是什么?

完整答案

主要区别

特性普通变量ThreadLocal
作用域类或方法作用域线程作用域
线程安全需要同步机制天然线程安全
数据共享所有线程共享每个线程独立
内存一份数据每个线程一份数据
使用场景单线程或需要共享多线程且需要隔离

详细对比

1. 数据共享

// 普通变量:所有线程共享
public class NormalVariable {
    private String data = "共享数据";
    
    public void test() {
        new Thread(() -> {
            data = "线程1修改";
            System.out.println(data);  // 可能被其他线程修改
        }).start();
        
        new Thread(() -> {
            data = "线程2修改";
            System.out.println(data);  // 可能被其他线程修改
        }).start();
    }
}

// ThreadLocal:每个线程独立
public class ThreadLocalVariable {
    private ThreadLocal<String> data = new ThreadLocal<>();
    
    public void test() {
        new Thread(() -> {
            data.set("线程1的数据");
            System.out.println(data.get());  // 总是:线程1的数据
        }).start();
        
        new Thread(() -> {
            data.set("线程2的数据");
            System.out.println(data.get());  // 总是:线程2的数据
        }).start();
    }
}

2. 线程安全

// 普通变量:需要同步
private int count = 0;

public void increment() {
    synchronized (this) {  // 需要同步
        count++;
    }
}

// ThreadLocal:天然线程安全
private ThreadLocal<Integer> count = new ThreadLocal<Integer>() {
    @Override
    protected Integer initialValue() {
        return 0;
    }
};

public void increment() {
    count.set(count.get() + 1);  // 不需要同步
}

总结: ThreadLocal提供线程局部变量,每个线程有独立副本,天然线程安全;普通变量所有线程共享,需要同步机制保证线程安全。

3. ThreadLocal的实现原理是什么?

完整答案

ThreadLocal的实现原理

ThreadLocal通过ThreadLocalMap实现,每个Thread对象内部有一个ThreadLocalMap,存储该线程的所有ThreadLocal变量。

核心原理

Thread
  └── ThreadLocalMap (每个线程一个)
       └── Entry[] (数组,存储ThreadLocal变量)
            └── Entry(ThreadLocal<?> k, Object v)

源码分析

// Thread类中有ThreadLocalMap
public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;  // 线程的ThreadLocalMap
}

// ThreadLocal.get()
public T get() {
    Thread t = Thread.currentThread();  // 1. 获取当前线程
    ThreadLocalMap map = getMap(t);     // 2. 获取该线程的Map
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);  // 3. 从该线程的Map获取值
        if (e != null) {
            return (T) e.value;
        }
    }
    return setInitialValue();
}

// ThreadLocal.set()
public void set(T value) {
    Thread t = Thread.currentThread();  // 1. 获取当前线程
    ThreadLocalMap map = getMap(t);     // 2. 获取该线程的Map
    if (map != null) {
        map.set(this, value);  // 3. 存储到该线程的Map
    } else {
        createMap(t, value);   // 4. 创建该线程的Map
    }
}

总结: ThreadLocal通过ThreadLocalMap实现线程隔离,每个线程有独立的ThreadLocalMap存储ThreadLocal变量,实现线程局部变量。

4. ThreadLocal如何实现线程隔离?

完整答案

线程隔离的实现机制

ThreadLocal通过每个线程独立的ThreadLocalMap实现线程隔离,每个线程存储自己的数据副本。

隔离机制

线程1
  └── ThreadLocalMap1
       └── Entry[ThreadLocal1 → "线程1的数据"]

线程2
  └── ThreadLocalMap2
       └── Entry[ThreadLocal1 → "线程2的数据"]

主线程
  └── ThreadLocalMap3
       └── Entry[ThreadLocal1 → "主线程的数据"]

源码分析

// ThreadLocal.get()
public T get() {
    Thread t = Thread.currentThread();  // 1. 获取当前线程
    ThreadLocalMap map = getMap(t);     // 2. 获取该线程的Map
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);  // 3. 从该线程的Map获取值
        if (e != null) {
            return (T) e.value;
        }
    }
    return setInitialValue();
}

// ThreadLocalMap存储在Thread对象中
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;  // 返回该线程的Map
}

代码示例

// 创建ThreadLocal
ThreadLocal<String> threadLocal = new ThreadLocal<>();

// 线程1
Thread thread1 = new Thread(() -> {
    threadLocal.set("线程1的数据");
    System.out.println("线程1: " + threadLocal.get());
    // 输出:线程1: 线程1的数据
});
thread1.start();

// 线程2
Thread thread2 = new Thread(() -> {
    threadLocal.set("线程2的数据");
    System.out.println("线程2: " + threadLocal.get());
    // 输出:线程2: 线程2的数据
});
thread2.start();

// 每个线程有自己独立的数据,互不干扰

关键点

  1. 每个线程独立的Map

    • Thread.threadLocals存储该线程的所有ThreadLocal变量
    • 不同线程的Map互不干扰
  2. 通过Thread.currentThread()获取

    • 总是获取当前线程的Map
    • 确保线程隔离
  3. 数据不共享

    • 每个线程的数据存储在各自的Map中
    • 线程间无法访问对方的数据

实际应用场景

场景1:存储用户信息

public class UserContext {
    private static ThreadLocal<User> userContext = new ThreadLocal<>();
    
    public static void setUser(User user) {
        userContext.set(user);  // 设置到当前线程
    }
    
    public static User getUser() {
        return userContext.get();  // 从当前线程获取
    }
    
    public static void clear() {
        userContext.remove();  // 从当前线程移除
    }
}

// 使用
// 线程1
new Thread(() -> {
    UserContext.setUser(user1);  // 线程1的数据
    User user = UserContext.getUser();  // 获取线程1的数据
}).start();

// 线程2
new Thread(() -> {
    UserContext.setUser(user2);  // 线程2的数据
    User user = UserContext.getUser();  // 获取线程2的数据
}).start();

场景2:Looper中的使用

// Android中Looper使用ThreadLocal实现线程绑定
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

public static void prepare() {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    // 为当前线程创建Looper
    sThreadLocal.set(new Looper(quitAllowed));
}

public static Looper myLooper() {
    // 获取当前线程的Looper
    return sThreadLocal.get();
}

总结: ThreadLocal通过每个线程独立的ThreadLocalMap实现线程隔离。每个线程有自己的Map,存储该线程的所有ThreadLocal变量。通过Thread.currentThread()确保获取到正确的线程Map,实现线程间数据互不干扰。

5. ThreadLocal的内存泄漏问题

完整答案

内存泄漏的原因

ThreadLocal可能导致内存泄漏的原因是Entry的value是强引用,即使ThreadLocal被回收,value仍然被引用,无法被GC回收。

泄漏机制

ThreadLocal (弱引用)
    ↓
ThreadLocalMap.Entry
    ├── key: ThreadLocal (弱引用) ✅ 可以被回收
    └── value: Object (强引用) ❌ 无法被回收

泄漏场景

// 场景:ThreadLocal使用完后没有remove()

public class MemoryLeakExample {
    private static ThreadLocal<Context> contextLocal = new ThreadLocal<>();
    
    public void setContext(Context context) {
        contextLocal.set(context);  // 设置值
    }
    
    public Context getContext() {
        return contextLocal.get();  // 获取值
    }
    
    // ❌ 问题:忘记remove()
    // ThreadLocal被回收,但value(Context)仍然被引用
    // 如果Context持有Activity引用,Activity无法被回收 → 内存泄漏
}

解决方案

方案1:使用完后及时remove()

public class SafeContextManager {
    private static ThreadLocal<Context> contextLocal = new ThreadLocal<>();
    
    public void setContext(Context context) {
        contextLocal.set(context);
    }
    
    public Context getContext() {
        return contextLocal.get();
    }
    
    // ✅ 使用完后清理
    public void clearContext() {
        contextLocal.remove();  // 清理ThreadLocal
    }
    
    // 或者在finally中清理
    public void processWithContext(Context context) {
        try {
            contextLocal.set(context);
            // 使用context
        } finally {
            contextLocal.remove();  // 确保清理
        }
    }
}

方案2:在线程池中使用时特别注意

// 线程池中的线程会复用,需要特别注意清理
ExecutorService executor = Executors.newFixedThreadPool(5);

executor.execute(() -> {
    try {
        threadLocal.set("数据");
        // 使用数据
    } finally {
        threadLocal.remove();  // ✅ 必须清理
    }
});

ThreadLocal的清理机制

// ThreadLocalMap中有清理机制(expungeStaleEntry)

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    
    // 1. 清理指定位置的Entry
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;
    
    // 2. 继续清理后面的Entry(重新hash)
    // ...
}

最佳实践

  1. 及时清理
// ✅ 推荐:使用try-finally确保清理
public void useThreadLocal() {
    try {
        threadLocal.set("数据");
        // 使用数据
    } finally {
        threadLocal.remove();  // 确保清理
    }
}
  1. 使用initialValue()
// ✅ 推荐:设置默认值
private static ThreadLocal<String> threadLocal = new ThreadLocal<String>() {
    @Override
    protected String initialValue() {
        return "";  // 默认值
    }
};
  1. 线程池中特别注意
// ✅ 线程池中必须清理
executor.execute(() -> {
    try {
        threadLocal.set("数据");
        // 使用
    } finally {
        threadLocal.remove();  // 必须清理
    }
});

总结: ThreadLocal的内存泄漏主要是由于Entry的value是强引用导致的。解决方案:使用完后及时remove()、在finally中清理、在线程池中特别注意清理。ThreadLocalMap有清理机制,但需要手动触发。


2.2 ThreadLocalMap 深入理解

6. ThreadLocalMap的实现原理是什么?

完整答案

ThreadLocalMap的定义

ThreadLocalMap是ThreadLocal的内部类,是ThreadLocal的核心数据结构,用于存储线程的ThreadLocal变量。

数据结构

// ThreadLocalMap使用数组存储Entry
static class ThreadLocalMap {
    // Entry数组(类似HashMap的数组)
    private Entry[] table;
    private int size = 0;
    private int threshold;  // 扩容阈值
    
    // Entry是键值对
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;  // 存储的值
        
        Entry(ThreadLocal<?> k, Object v) {
            super(k);  // ThreadLocal作为key,使用弱引用
            value = v;  // value是强引用
        }
    }
}

核心特点

  1. 数组结构

    • 使用Entry[]数组存储数据
    • 类似HashMap,但更简单
  2. 哈希算法

    • 使用ThreadLocal的hashCode计算索引
    • 使用线性探测法解决冲突
  3. 弱引用

    • Entry的key(ThreadLocal)是弱引用
    • Entry的value是强引用

存储结构

Thread
  └── threadLocals: ThreadLocalMap
       └── table: Entry[]
            ├── [0] Entry(ThreadLocal1, "值1")
            ├── [1] Entry(ThreadLocal2, "值2")
            ├── [2] null
            ├── [3] Entry(ThreadLocal3, "值3")
            └── ...

哈希算法

// ThreadLocal的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);
}

// ThreadLocalMap计算索引
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);  // 线性探测
}

冲突解决机制

ThreadLocalMap使用线性探测法(开放地址法)解决哈希冲突:

// ThreadLocalMap.set()
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) {
            // 找到相同的key,更新value
            e.value = value;
            return;
        }
        if (k == null) {
            // key被回收(弱引用),替换过期Entry
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    
    // 找到空位置,插入新Entry
    tab[i] = new Entry(key, value);
    size++;
}

为什么使用线性探测法

  1. 简单高效

    • 实现简单,不需要链表
    • 内存连续,缓存友好
  2. 适合场景

    • ThreadLocal数量通常不多
    • 冲突概率较低
  3. 避免额外内存

    • 不需要链表节点
    • 节省内存

总结: ThreadLocalMap使用数组+线性探测法实现,Entry的key是弱引用,value是强引用。使用黄金分割数计算hashCode,通过线性探测解决冲突。

7. ThreadLocalMap.Entry为什么使用弱引用?

完整答案

Entry的结构

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    
    Entry(ThreadLocal<?> k, Object v) {
        super(k);  // ThreadLocal作为key,使用弱引用
        value = v;  // value是强引用
    }
}

为什么key使用弱引用

  1. 防止内存泄漏

    • 如果ThreadLocal是强引用,即使ThreadLocal对象不再使用,也无法被GC回收
    • 使用弱引用,ThreadLocal对象可以被GC回收
  2. 自动清理机制

    • 当ThreadLocal被回收后,Entry的key变为null
    • ThreadLocalMap可以检测到过期Entry并清理

引用关系

强引用链:
Thread → ThreadLocalMap → Entry[] → Entry → value (强引用)

弱引用链:
ThreadLocal (外部引用) → Entry.key (弱引用)

如果外部不再引用ThreadLocal:
ThreadLocal可以被GC回收 → Entry.key变为null → 可以清理Entry

内存泄漏场景

// 场景:ThreadLocal使用完后,外部引用被清除

public class ThreadLocalExample {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    
    public void useThreadLocal() {
        threadLocal.set("数据");
        // 使用数据
    }
    
    // 如果threadLocal是强引用:
    // ThreadLocal对象无法被GC回收
    // Entry无法被清理 → 内存泄漏
    
    // 如果threadLocal是弱引用(实际实现):
    // ThreadLocal对象可以被GC回收
    // Entry.key变为null → 可以清理Entry
}

为什么value是强引用

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

问题

  • value是强引用,即使ThreadLocal被回收,value仍然无法被GC回收
  • 这是ThreadLocal内存泄漏的根本原因

解决方案

// ✅ 使用完后及时remove()
threadLocal.set("数据");
try {
    // 使用数据
} finally {
    threadLocal.remove();  // 清理value
}

清理机制

// ThreadLocalMap.expungeStaleEntry()
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    
    // 清理过期Entry
    tab[staleSlot].value = null;  // 清理value
    tab[staleSlot] = null;
    size--;
    
    // 继续清理后面的Entry(重新hash)
    // ...
}

总结: ThreadLocalMap.Entry的key使用弱引用,防止ThreadLocal对象无法被GC回收。但value是强引用,可能导致内存泄漏,需要及时调用remove()清理。


2.3 ThreadLocal 使用场景和方法

8. ThreadLocal的使用场景有哪些?

完整答案

主要使用场景

ThreadLocal适用于需要线程隔离的场景,每个线程需要独立的数据副本。

场景1:存储线程上下文信息

// 存储请求上下文
public class RequestContext {
    private static ThreadLocal<Request> requestContext = new ThreadLocal<>();
    
    public static void setRequest(Request request) {
        requestContext.set(request);
    }
    
    public static Request getRequest() {
        return requestContext.get();
    }
    
    public static void clear() {
        requestContext.remove();
    }
}

// 使用
// 线程1处理请求1
new Thread(() -> {
    RequestContext.setRequest(request1);
    processRequest();  // 可以随时获取request1
}).start();

// 线程2处理请求2
new Thread(() -> {
    RequestContext.setRequest(request2);
    processRequest();  // 可以随时获取request2
}).start();

场景2:存储用户信息

// 存储当前用户信息
public class UserContext {
    private static ThreadLocal<User> userContext = new ThreadLocal<>();
    
    public static void setUser(User user) {
        userContext.set(user);
    }
    
    public static User getUser() {
        return userContext.get();
    }
    
    public static void clear() {
        userContext.remove();
    }
}

// 使用
// 每个请求线程存储自己的用户信息
public void handleRequest(Request request) {
    User user = authenticate(request);
    UserContext.setUser(user);
    
    try {
        // 在处理过程中可以随时获取用户信息
        processRequest();
    } finally {
        UserContext.clear();  // 清理
    }
}

场景3:存储数据库连接

// 存储数据库连接(每个线程一个连接)
public class DatabaseManager {
    private static ThreadLocal<Connection> connectionContext = new ThreadLocal<>();
    
    public static Connection getConnection() {
        Connection conn = connectionContext.get();
        if (conn == null) {
            conn = createConnection();
            connectionContext.set(conn);
        }
        return conn;
    }
    
    public static void closeConnection() {
        Connection conn = connectionContext.get();
        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            connectionContext.remove();
        }
    }
}

场景4:Android中Looper的使用

// Android中Looper使用ThreadLocal实现线程绑定
public final class Looper {
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    
    public static void prepare() {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }
    
    public static Looper myLooper() {
        return sThreadLocal.get();
    }
}

场景5:存储日期格式化器

// SimpleDateFormat不是线程安全的,使用ThreadLocal
public class DateFormatter {
    private static ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };
    
    public static String format(Date date) {
        return formatter.get().format(date);
    }
}

适用场景总结

场景是否适用说明
线程上下文信息✅ 适用每个线程需要独立的上下文
用户会话信息✅ 适用每个线程需要独立的用户信息
数据库连接✅ 适用每个线程需要独立的连接
日期格式化器✅ 适用避免线程安全问题
全局变量❌ 不适用需要所有线程共享
需要线程间通信❌ 不适用ThreadLocal是隔离的,不能通信

不适用场景

  1. 需要线程间共享数据

    • ThreadLocal是隔离的,不能用于线程间通信
  2. 需要全局变量

    • 应该使用普通变量或单例
  3. 需要传递参数

    • 应该使用方法参数传递

总结: ThreadLocal适用于需要线程隔离的场景,如线程上下文、用户信息、数据库连接、日期格式化器等。不适合需要线程间共享或通信的场景。

9. ThreadLocal.remove()的作用是什么?为什么必须调用?

完整答案

remove()的作用

remove()方法用于清除当前线程的ThreadLocal变量,释放内存,防止内存泄漏。

方法定义

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null) {
        m.remove(this);  // 从ThreadLocalMap中移除Entry
    }
}

为什么必须调用

  1. 防止内存泄漏

    • Entry的value是强引用,即使ThreadLocal被回收,value仍然无法被GC回收
    • 必须调用remove()清理value
  2. 线程池场景

    • 线程池中的线程会复用
    • 如果不清理,上一个任务的数据会污染下一个任务
  3. 及时释放内存

    • ThreadLocalMap会一直持有value的引用
    • 及时清理可以释放内存

内存泄漏示例

// ❌ 错误:没有调用remove()
public class MemoryLeakExample {
    private static ThreadLocal<Context> contextLocal = new ThreadLocal<>();
    
    public void processRequest(Context context) {
        contextLocal.set(context);  // 设置值
        // 使用context
        // ❌ 忘记remove(),value无法被GC回收
    }
}

// ✅ 正确:调用remove()
public class SafeExample {
    private static ThreadLocal<Context> contextLocal = new ThreadLocal<>();
    
    public void processRequest(Context context) {
        try {
            contextLocal.set(context);
            // 使用context
        } finally {
            contextLocal.remove();  // ✅ 确保清理
        }
    }
}

线程池中的问题

// ❌ 问题:线程池中不清理会导致数据污染
ExecutorService executor = Executors.newFixedThreadPool(5);
ThreadLocal<String> threadLocal = new ThreadLocal<>();

executor.execute(() -> {
    threadLocal.set("任务1的数据");
    // 处理任务1
    // ❌ 没有remove(),数据残留在线程中
});

executor.execute(() -> {
    // 同一个线程处理任务2
    String data = threadLocal.get();  // ❌ 可能获取到任务1的数据
    // 处理任务2
});

// ✅ 正确:使用finally清理
executor.execute(() -> {
    try {
        threadLocal.set("任务1的数据");
        // 处理任务1
    } finally {
        threadLocal.remove();  // ✅ 必须清理
    }
});

最佳实践

// ✅ 方式1:使用try-finally
public void useThreadLocal() {
    try {
        threadLocal.set("数据");
        // 使用数据
    } finally {
        threadLocal.remove();  // 确保清理
    }
}

// ✅ 方式2:在方法结束时清理
public void processRequest() {
    threadLocal.set("数据");
    try {
        // 使用数据
    } finally {
        threadLocal.remove();  // 清理
    }
}

// ✅ 方式3:使用工具类封装
public class ThreadLocalManager {
    public static <T> void withThreadLocal(ThreadLocal<T> threadLocal, T value, Runnable task) {
        try {
            threadLocal.set(value);
            task.run();
        } finally {
            threadLocal.remove();
        }
    }
}

// 使用
ThreadLocalManager.withThreadLocal(threadLocal, "数据", () -> {
    // 使用数据,自动清理
});

源码分析

// ThreadLocal.remove()
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null) {
        m.remove(this);  // 从Map中移除Entry
    }
}

// ThreadLocalMap.remove()
private void remove(ThreadLocal<?> key) {
    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)]) {
        if (e.get() == key) {
            e.clear();  // 清除弱引用
            expungeStaleEntry(i);  // 清理过期Entry
            return;
        }
    }
}

总结: ThreadLocal.remove()用于清除当前线程的ThreadLocal变量,防止内存泄漏。在finally中调用remove()是最佳实践,特别是在线程池场景中必须清理。

10. ThreadLocal.initialValue()的作用是什么?

完整答案

initialValue()的作用

initialValue()方法用于设置ThreadLocal的初始值,当第一次调用get()时,如果值不存在,会调用initialValue()获取初始值。

方法定义

protected T initialValue() {
    return null;  // 默认返回null
}

使用方式

// 方式1:重写initialValue()
ThreadLocal<String> threadLocal = new ThreadLocal<String>() {
    @Override
    protected String initialValue() {
        return "默认值";  // 设置初始值
    }
};

// 使用
String value = threadLocal.get();  // 返回"默认值",不需要先set()

// 方式2:使用withInitial()(Java 8+)
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "默认值");

使用场景

场景1:设置默认值

// 计数器,初始值为0
ThreadLocal<Integer> counter = new ThreadLocal<Integer>() {
    @Override
    protected Integer initialValue() {
        return 0;  // 初始值为0
    }
};

// 使用
counter.set(counter.get() + 1);  // 不需要先判断是否为null

场景2:日期格式化器

// 日期格式化器,每个线程一个
ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>() {
    @Override
    protected SimpleDateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    }
};

// 使用
String dateStr = formatter.get().format(new Date());  // 直接使用,不需要先创建

场景3:避免空指针异常

// 设置默认值,避免null
ThreadLocal<String> name = new ThreadLocal<String>() {
    @Override
    protected String initialValue() {
        return "未知";  // 默认值
    }
};

// 使用
String userName = name.get();  // 不会返回null,返回"未知"
if (!userName.equals("未知")) {  // 不需要null检查
    // 处理
}

与set()的区别

// 方式1:使用initialValue()
ThreadLocal<String> threadLocal = new ThreadLocal<String>() {
    @Override
    protected String initialValue() {
        return "默认值";
    }
};
String value = threadLocal.get();  // 返回"默认值"

// 方式2:使用set()
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("默认值");
String value = threadLocal.get();  // 返回"默认值"

优势

  1. 延迟初始化

    • 只有在第一次get()时才创建对象
    • 节省内存
  2. 避免空指针

    • 总是有值,不需要null检查
  3. 代码简洁

    • 不需要先set()再get()

源码分析

// ThreadLocal.get()
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            return (T) e.value;
        }
    }
    return setInitialValue();  // 如果不存在,调用setInitialValue()
}

// ThreadLocal.setInitialValue()
private T setInitialValue() {
    T value = initialValue();  // 调用initialValue()获取初始值
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
    return value;
}

总结: ThreadLocal.initialValue()用于设置ThreadLocal的初始值,当第一次get()时自动调用。适合设置默认值、延迟初始化、避免空指针等场景。


2.4 InheritableThreadLocal

11. InheritableThreadLocal是什么?它和ThreadLocal的区别是什么?

完整答案

InheritableThreadLocal的定义

InheritableThreadLocal是ThreadLocal的子类,它允许子线程继承父线程的ThreadLocal变量

主要区别

特性ThreadLocalInheritableThreadLocal
继承性子线程无法继承父线程的值子线程可以继承父线程的值
使用场景线程隔离父子线程传递数据
实现方式ThreadLocalMapInheritableThreadLocalMap

代码示例

// ThreadLocal:子线程无法继承
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("父线程的数据");

new Thread(() -> {
    String value = threadLocal.get();  // 返回null
    System.out.println(value);
}).start();

// InheritableThreadLocal:子线程可以继承
InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
inheritableThreadLocal.set("父线程的数据");

new Thread(() -> {
    String value = inheritableThreadLocal.get();  // 返回"父线程的数据"
    System.out.println(value);
}).start();

实现原理

// InheritableThreadLocal
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    protected T childValue(T parentValue) {
        return parentValue;  // 子线程继承父线程的值
    }
    
    ThreadLocalMap getMap(Thread t) {
        return t.inheritableThreadLocals;  // 使用inheritableThreadLocals
    }
    
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

// Thread类中有两个Map
public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;  // ThreadLocal使用
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;  // InheritableThreadLocal使用
}

使用场景

场景1:传递请求上下文

// 传递请求ID到子线程
InheritableThreadLocal<String> requestId = new InheritableThreadLocal<>();

// 主线程
requestId.set("request-123");

// 创建子线程处理请求
new Thread(() -> {
    String id = requestId.get();  // 可以获取到"request-123"
    processRequest(id);
}).start();

场景2:传递用户信息

// 传递用户信息到子线程
InheritableThreadLocal<User> userContext = new InheritableThreadLocal<>();

// 主线程
userContext.set(currentUser);

// 子线程可以获取用户信息
new Thread(() -> {
    User user = userContext.get();  // 可以获取到currentUser
    // 处理任务
}).start();

注意事项

  1. 只在创建时继承

    • 只有在创建子线程时才会继承
    • 如果父线程之后修改值,子线程不会看到
  2. 线程池问题

    • 线程池中的线程是复用的
    • 可能获取到上一个任务的数据
  3. 内存泄漏

    • 同样需要调用remove()清理

总结: InheritableThreadLocal允许子线程继承父线程的ThreadLocal变量,适合父子线程传递数据的场景。但需要注意线程池复用和内存泄漏问题。


2.5 ThreadLocal 清理机制和最佳实践

12. ThreadLocalMap的清理机制(expungeStaleEntry)是什么?

完整答案

清理机制的作用

expungeStaleEntry()是ThreadLocalMap的核心清理方法,用于清理过期的Entry(key为null的Entry),防止内存泄漏。

过期Entry的产生

// Entry的key是弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;  // value是强引用
    
    Entry(ThreadLocal<?> k, Object v) {
        super(k);  // key是弱引用
        value = v;  // value是强引用
    }
}

// 当ThreadLocal对象被GC回收后
ThreadLocal threadLocal = new ThreadLocal<>();
threadLocal.set("数据");
threadLocal = null;  // 外部引用被清除

// ThreadLocal对象被GC回收 → Entry.key变为null → Entry变成过期Entry

expungeStaleEntry()的实现

// ThreadLocalMap.expungeStaleEntry()
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    
    // 1. 清理指定位置的过期Entry
    tab[staleSlot].value = null;  // 清理value(强引用)
    tab[staleSlot] = null;        // 清理Entry
    size--;
    
    // 2. 重新hash后面的Entry(因为线性探测,后面的Entry可能受影响)
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            // 如果后面的Entry也是过期的,继续清理
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // 如果后面的Entry不是过期的,重新计算位置
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                // 当前位置不是正确位置,需要移动
                tab[i] = null;  // 清空当前位置
                
                // 重新插入到正确位置
                while (tab[h] != null) {
                    h = nextIndex(h, len);
                }
                tab[h] = e;
            }
        }
    }
    return i;  // 返回下一个空位置
}

清理时机

  1. 调用remove()时
// ThreadLocal.remove()
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null) {
        m.remove(this);  // 内部会调用expungeStaleEntry()
    }
}
  1. set()时发现过期Entry
// ThreadLocalMap.set()
private void set(ThreadLocal<?> key, Object value) {
    // ...
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            // 发现过期Entry,清理并替换
            replaceStaleEntry(key, value, i);  // 内部调用expungeStaleEntry()
            return;
        }
    }
}
  1. get()时发现过期Entry
// ThreadLocalMap.getEntry()
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key) {
        return e;
    } else {
        return getEntryAfterMiss(key, i, e);  // 可能触发清理
    }
}

清理流程

1. 发现过期Entry(key为null)
    ↓
2. 调用expungeStaleEntry()
    ↓
3. 清理过期Entry的value
    ↓
4. 重新hash后面的Entry(因为线性探测)
    ↓
5. 继续清理后面的过期Entry
    ↓
6. 返回下一个空位置

为什么需要重新hash

// 线性探测示例
// 初始状态:
// [0] Entry(ThreadLocal1, "值1")
// [1] Entry(ThreadLocal2, "值2")  // ThreadLocal2的hashCode是1,但位置0被占用,所以放在位置1
// [2] null

// 如果ThreadLocal1被回收,位置0变成过期Entry:
// [0] Entry(null, "值1")  // 过期Entry
// [1] Entry(ThreadLocal2, "值2")
// [2] null

// 清理位置0后,需要重新hash位置1的Entry:
// 检查ThreadLocal2的正确位置是否是1
// 如果是,保持不变
// 如果不是,移动到正确位置

总结: expungeStaleEntry()用于清理过期Entry,防止内存泄漏。它会清理value、重新hash后面的Entry,确保线性探测的正确性。在remove()、set()、get()时都可能触发清理。

13. ThreadLocal在线程池中的正确使用方式是什么?

完整答案

线程池中的问题

线程池中的线程是复用的,如果不清理ThreadLocal,上一个任务的数据会污染下一个任务。

问题示例

// ❌ 错误:线程池中不清理ThreadLocal
ExecutorService executor = Executors.newFixedThreadPool(5);
ThreadLocal<String> threadLocal = new ThreadLocal<>();

executor.execute(() -> {
    threadLocal.set("任务1的数据");
    // 处理任务1
    // ❌ 没有remove(),数据残留
});

// 同一个线程处理任务2
executor.execute(() -> {
    String data = threadLocal.get();  // ❌ 可能获取到"任务1的数据"
    // 处理任务2,数据被污染
});

正确使用方式

方式1:使用try-finally清理(推荐)

ExecutorService executor = Executors.newFixedThreadPool(5);
ThreadLocal<String> threadLocal = new ThreadLocal<>();

executor.execute(() -> {
    try {
        threadLocal.set("任务1的数据");
        // 处理任务1
    } finally {
        threadLocal.remove();  // ✅ 必须清理
    }
});

方式2:封装工具类

// 封装ThreadLocal工具类
public class ThreadLocalManager {
    public static <T> void withThreadLocal(ThreadLocal<T> threadLocal, T value, Runnable task) {
        try {
            threadLocal.set(value);
            task.run();
        } finally {
            threadLocal.remove();  // 自动清理
        }
    }
    
    public static <T, R> R withThreadLocal(ThreadLocal<T> threadLocal, T value, Supplier<R> supplier) {
        try {
            threadLocal.set(value);
            return supplier.get();
        } finally {
            threadLocal.remove();  // 自动清理
        }
    }
}

// 使用
ThreadLocal<String> threadLocal = new ThreadLocal<>();
executor.execute(() -> {
    ThreadLocalManager.withThreadLocal(threadLocal, "数据", () -> {
        // 处理任务,自动清理
    });
});

方式3:使用ThreadLocal的initialValue()

// 使用initialValue()设置默认值
ThreadLocal<String> threadLocal = new ThreadLocal<String>() {
    @Override
    protected String initialValue() {
        return "";  // 默认值
    }
};

// 但这样仍然需要清理,因为可能设置了非默认值
executor.execute(() -> {
    try {
        threadLocal.set("任务数据");
        // 处理任务
    } finally {
        threadLocal.remove();  // ✅ 仍然需要清理
    }
});

InheritableThreadLocal在线程池中的问题

// ❌ 问题:InheritableThreadLocal在线程池中会继承上一个任务的数据
InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();

// 主线程
inheritableThreadLocal.set("主线程数据");

// 线程池中的线程可能获取到上一个任务的数据
executor.execute(() -> {
    String data = inheritableThreadLocal.get();  // ❌ 可能获取到上一个任务的数据
    // 处理任务
});

// ✅ 解决:必须清理
executor.execute(() -> {
    try {
        // 处理任务
    } finally {
        inheritableThreadLocal.remove();  // ✅ 必须清理
    }
});

最佳实践

  1. 总是使用try-finally
executor.execute(() -> {
    try {
        threadLocal.set("数据");
        // 处理任务
    } finally {
        threadLocal.remove();  // ✅ 确保清理
    }
});
  1. 封装工具类
// 封装后使用更安全
ThreadLocalManager.withThreadLocal(threadLocal, "数据", () -> {
    // 处理任务
});
  1. 避免在长时间运行的线程中使用
// ❌ 避免:在长时间运行的线程中累积数据
Thread workerThread = new Thread(() -> {
    while (true) {
        threadLocal.set("数据");
        // 处理任务
        // ❌ 没有清理,数据累积
    }
});

// ✅ 推荐:每个任务都清理
Thread workerThread = new Thread(() -> {
    while (true) {
        try {
            threadLocal.set("数据");
            // 处理任务
        } finally {
            threadLocal.remove();  // ✅ 每个任务都清理
        }
    }
});

总结: ThreadLocal在线程池中必须使用try-finally清理,防止数据污染。可以封装工具类简化使用。InheritableThreadLocal在线程池中也需要清理。