大家好,今天我们一起聊聊Java开发中常见的线程本地变量ThreadLocal。
大纲

简介
ThreadLocal 是 Java 中一个用于解决多线程并发问题的工具类,它提供了线程本地变量。这些变量不同于它们的正常变量副本,因为访问一个 ThreadLocal 变量的线程(通过其 get 或 set 方法)都会拥有自己的独立初始化的变量副本。ThreadLocal 实例通常被声明为 private static,这样它们就只会与特定的线程关联,而不是与特定的类实例关联。
图解

Thread源码
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
栗子
不同线程中保存和获取变量,代码
public class ThreadLocalTest {
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
@Test
public void threadLocalTest01(){
Thread thdA = new Thread(()->{
threadLocal.set("ThreadA: "+Thread.currentThread().getName());
System.out.println("线程A本地变量中的值为:"+threadLocal.get());
});
Thread thdB = new Thread(()->{
threadLocal.set("ThreadB: "+Thread.currentThread().getName());
System.out.println("线程B本地变量中的值为:"+threadLocal.get());
});
thdA.start();
thdB.start();
}
}
不同线程中保存和获取变量,运行结果
线程B本地变量中的值为:ThreadB: Thread-1
线程A本地变量中的值为:ThreadA: Thread-0
进程已结束,退出代码 0
一个线程删除变量,不影响另一个线程,代码
@Test
public void threadLocalTest02(){
Thread thdA = new Thread(()->{
threadLocal.set("ThreadA: "+Thread.currentThread().getName());
System.out.println("线程A本地变量中的值为:"+threadLocal.get());
threadLocal.remove();
System.out.println("线程A删除本地变量中值后ThreadLocal中的值为:"+threadLocal.get());
});
Thread thdB = new Thread(()->{
threadLocal.set("ThreadB: "+Thread.currentThread().getName());
System.out.println("线程B本地变量中的值为:"+threadLocal.get());
System.out.println("线程B未删除本地变量:"+threadLocal.get());
});
thdA.start();
thdB.start();
}
一个线程删除变量,不影响另一个线程,运行结果:
线程A本地变量中的值为:ThreadA: Thread-0
线程B本地变量中的值为:ThreadB: Thread-1
线程B未删除本地变量:ThreadB: Thread-1
线程A删除本地变量中值后ThreadLocal中的值为:null
进程已结束,退出代码 0
通过上述栗子,我们可以看出,线程A和线程B存储在ThreadLocal中的变量互不干扰,线程A存储的变量只能由线程A访问,线程B存 储的变量只能由线程B访问。
基本概念
线程局部变量
ThreadLocal 为每个线程提供了一个独立的变量副本,各线程之间互不干扰,实现了线程安全的访问共享变量。
内存泄漏
如果使用不当,ThreadLocal 可能会导致线程内存泄漏的问题。由于 ThreadLocalMap 中的 Entry 弱引用只有在下一次 GC(垃圾收集)时才会被回收,因此如果没有及时清理 ThreadLocal 变量,就可能会导致内存泄漏。
高效性
ThreadLocal 的高效性主要体现在它减少了线程同步所带来的开销,从而提高了程序的运行效率。
方法和特性
构造函数
ThreadLocal<T>
源码
/**
* Creates a thread local variable.
* @see #withInitial(java.util.function.Supplier)
*/
public ThreadLocal() {
}
set(T value)
设置当前线程的线程局部变量的值。
源码
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
首先通过getMap方法,获取当前线程的ThreadLocalMap对象,如果ThreadLocalMap对象存在,则直接设置对应的value值,其中key为ThreadLocal对象,value为要设置的值;若TreadLocalMap对象不存在,则通过createMap方法,初始化ThreadLocalMap对象并设置对应的value值。
get()
返回当前线程所对应的线程局部变量的值。
源码
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
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();
}
首先通过getMap方法,获取当前线程的ThreadLocalMap对象,如果ThreadLocalMap对象已创建,则通过key值,即ThreadLocal对象,获取ThreadLocalMap的Entry对象,从而获取对应的value值;如果ThreadLocalMap未创建,则创建ThreadLocalMap对象,并返回初始化value值。
initialValue()
返回此线程局部变量的初始值。这是一个延迟调用方法,在一个线程首次调用 get() 或 set(T) 时才执行,并且仅执行一次。
源码
/**
* Returns the current thread's "initial value" for this
* thread-local variable. This method will be invoked the first
* time a thread accesses the variable with the {@link #get}
* method, unless the thread previously invoked the {@link #set}
* method, in which case the {@code initialValue} method will not
* be invoked for the thread. Normally, this method is invoked at
* most once per thread, but it may be invoked again in case of
* subsequent invocations of {@link #remove} followed by {@link #get}.
*
* <p>This implementation simply returns {@code null}; if the
* programmer desires thread-local variables to have an initial
* value other than {@code null}, {@code ThreadLocal} must be
* subclassed, and this method overridden. Typically, an
* anonymous inner class will be used.
*
* @return the initial value for this thread-local
*/
protected T initialValue() {
return null;
}
初始化值,默认为null,实际使用时,可根据具体使用场景,通过重写的方式,设置对应的初始化值。如Integer设置初始化值为0等。
remove()
JDK 1.5 新增,移除此线程局部变量的值。这个方法可以帮助减少内存占用,但在线程结束时,线程局部变量的值会自动被垃圾回收,所以显式调用此方法并不是必须的。
源码
/**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* {@code initialValue} method in the current thread.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
首先通过getMap方法,获取当前线程的ThreadLocalMap对象,然后通过ThreadLocalMap的remove方法,删除ThreadLocal对象对应的key。
getMap(Thread t)
获取指定线程关联的ThreadLocalMap对象。
源码
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
获取指定线程的ThreadLocalMap对象,每个线程均有自己的ThreadLocalMap对象,通过调用线程的属性threadLocals获取。
setInitialValue()
设置初始化数据
源码
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
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;
}
该方法为设置初始化值,先通过initialValue()方法,获取初始化值,然后通过getMap()方法,获取当前线程的ThreadLocalMap对象,如果为第一次调用,则通过createMap(Thread t, T value)方法创建ThreadLocalMap对象,并初始化值;如果非第一次调用,即已经创建了ThreadLocalMap对象,直接设置对应的值。
createMap(Thread t, T firstValue)
创建指定线程关联的ThreadLocalMap对象,并初始化局部变量信息。
源码
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
通过ThreadLocalMap的构造函数,创建ThreadLocalMap对象,并将指定线程t的threadLocals指向新创建的ThreadLocalMap对象。该构造函数调用时,会初始化key和value键值对,其中key为ThreadLocal对象,value为设置的firstValue值。
使用场景
线程上下文信息存储
例如在Web开发中,Spring框架利用ThreadLocal来存储请求级别的数据,如用户登录信息、事务上下文等,使得在处理请求的整个过程链路中都能方便地访问这些信息。
栗子
import java.util.HashMap;
import java.util.Map;
public class UserContext {
// 使用ThreadLocal来存储每个线程特有的用户会话信息
private static final ThreadLocal<Map<String, Object>> threadLocalContext = new ThreadLocal<>();
// 设置当前线程的用户上下文信息
public static void setUserContext(Map<String, Object> context) {
threadLocalContext.set(context);
}
// 获取当前线程的用户上下文信息
public static Map<String, Object> getUserContext() {
return threadLocalContext.get();
}
// 添加或更新用户会话信息中的特定属性
public static void put(String key, Object value) {
Map<String, Object> context = threadLocalContext.get();
if (context == null) {
context = new HashMap<>();
threadLocalContext.set(context);
}
context.put(key, value);
}
// 从用户会话信息中获取特定属性
public static Object get(String key) {
Map<String, Object> context = threadLocalContext.get();
return context != null ? context.get(key) : null;
}
// 清除当前线程的用户上下文信息
public static void clear() {
threadLocalContext.remove();
}
// 示例:在请求处理开始时设置用户会话信息
public static void startUserSession(String userId, String username) {
Map<String, Object> context = new HashMap<>();
context.put("userId", userId);
context.put("username", username);
setUserContext(context);
}
// 示例:在请求处理结束时清除用户会话信息
public static void endUserSession() {
clear();
}
}
在这个例子中,我们创建了一个UserContext类,其中的threadLocalContext是一个ThreadLocal变量,它持有一个Map,用于存储用户的会话信息。在处理HTTP请求时,可以在过滤器或拦截器中设置用户会话信息(如用户的ID和用户名),然后在后续的服务方法或处理器中通过getUserContext方法获取这些信息,而无需将这些信息作为参数传递。在请求处理结束后,通过调用clear方法来清理线程局部的用户上下文信息,防止内存泄漏。
避免线程同步开销
当某个变量只在单个线程内使用,不需要跨线程共享时,通过ThreadLocal可以避免使用synchronized或Lock等同步机制,提高性能。
栗子
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executos;
public class ThreadLocalExample {
// 定义一个ThreadLocal变量用来存储线程局部的计数器
private static final ThreadLocal<Integer> threadLocalCounter = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0; // 初始化每个线程的计数器为0
}
};
// 一个会被多个线程调用的方法,其中包含了对计数器的操作
public static void incrementCounter() {
// 获取当前线程的局部计数器,并加1
int count = threadLocalCounter.get();
threadLocalCounter.set(count + 1);
System.out.println(Thread.currentThread().getName() + ": Counter is now " + count + 1);
}
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交多个任务到线程池
for (int i = 0; i < 10; i++) {
executor.submit(() -> incrementCounter());
}
// 关闭线程池
executor.shutdown();
}
}
在这个例子中,我们创建了一个ThreadLocal
当程序运行时,尽管有多个线程同时调用incrementCounter方法,但由于每个线程都在各自的ThreadLocal变量上进行操作,所以不会发生线程间的同步开销。输出将显示每个线程各自的计数器值,它们各自独立递增,不受其他线程的影响。
简化参数传递
在一个长流程的方法调用链中,若有一个变量需要在多个方法间共享,但又不想通过参数层层传递,那么可以借助ThreadLocal存储。
栗子
import java.util.concurrent.atomic.AtomicInteger;
// 假设有一个UserContext类,代表用户上下文信息
public class UserContext {
private static final ThreadLocal<UserInfo> userInfoHolder = new ThreadLocal<>();
public static void setUserInfo(UserInfo userInfo) {
userInfoHolder.set(userInfo);
}
public static UserInfo getUserInfo() {
return userInfoHolder.get();
}
// 用户信息类
public static class UserInfo {
private String userId;
private String userName;
// 构造函数、getter和setter省略...
}
}
// 示例服务类,在处理请求时会用到用户信息
public class SomeService {
public void handleRequest() {
// 假设在某个地方(比如拦截器或者过滤器)已经设置了用户信息
UserContext.setUserInfo(new UserContext.UserInfo("user1", "User One"));
// 在处理请求的业务逻辑中,无需传入userId作为参数,可以直接从ThreadLocal获取
processBusinessLogic();
// 处理完请求后,可以清除ThreadLocal中的用户信息以防止内存泄露
UserContext.userInfoHolder.remove();
}
private void processBusinessLogic() {
// 直接从ThreadLocal中获取当前线程的用户信息
UserInfo currentUser = UserContext.getUserInfo();
System.out.println("Processing request for user: " + currentUser.getUserId());
// ...其他业务逻辑
}
}
// 使用
public static void main(String[] args) {
SomeService service = new SomeService();
service.handleRequest();
}
在这个例子中,UserContext类通过一个静态的ThreadLocal
注意事项
● ThreadLocal不是为了解决多线程共享变量的问题,而是为了在多线程中提供线程内的共享变量。
● 如果线程结束,ThreadLocal中存储的变量副本不会自动清理,可能会造成内存泄漏。在不再需要时,应该显式调用remove()方法释放资源。
● ThreadLocal的一个常见问题是,如果大量使用并且没有及时清理,可能导致ThreadLocalMap过大,尤其是在长时间运行的守护线程中。
● 不支持继承,子线程默认并不会继承父线程的ThreadLocal变量值,除非明确调用了inheritThreadLocals方法并在创建子线程时指定为true。
总结
ThreadLocal作为一种强大的工具类,为我们提供了一种解决多线程并发问题的新思路,有效地帮助开发者在多线程编程中管理那些仅在单线程内使用的共享数据,减少了并发编程中的复杂性和潜在风险。然而,我们也需要注意其可能带来的内存泄漏问题,并在使用时采取适当的措施来避免这些问题。
原文始发于微信公众号(扬哥手记):TheadLocal都有哪些需掌握的知识
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/288368.html