Java并发-以空间换时间之ThreadLocal
多线程同时对资源进行访问,为了线程安全,最简单的做法就是使用synchronized关键字,这是一种时间换空间的做法。
在注重性能的场合,多线程竞争问题显得愈发突出。当请求时间变长,用户体验变差,流失率增高。
有没有办法解决这个问题呢?Java提供了一种以空间换时间的容器,它就是ThreadLocal
。
ThreadLocal
通过为每个线程维护独立的变量副本,避免了线程间的竞争,从而提高了并发性能。这种设计模式在高并发场景下特别有效,因为它完全消除了同步的开销。
# 一、ThreadLocal
介绍
ThreadLocal
为每个线程维护一份独立的变量副本,每个线程都有自己的ThreadLocalMap
来存储数据,从而实现了线程隔离。
# 1、核心API
ThreadLocal
提供了以下核心方法:
set(T value)
- 设置当前线程的线程局部变量的值get()
- 返回当前线程所对应的线程局部变量remove()
- 删除当前线程所对应的线程局部变量(重要:防止内存泄漏)initialValue()
- 返回线程局部变量的初始值(protected方法,可重写)withInitial(Supplier<? extends T> supplier)
- Java 8引入的静态工厂方法
# 2、底层实现原理
ThreadLocal
的核心在于每个Thread
对象都有一个ThreadLocalMap
类型的成员变量threadLocals
:
public class Thread {
// 每个线程都有自己的ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals = null;
// ...
}
ThreadLocalMap
是ThreadLocal
的静态内部类,它使用线性探测法解决哈希冲突:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // key是弱引用
value = v; // value是强引用
}
}
private Entry[] table; // 存储数据的数组
}
# 3、使用示例:数据库连接管理
以数据库连接为例,它本身是非线程安全的:
public class TopicDao {
private Connection connection; // 线程不安全:多线程共享同一个连接
public void addTopic() throws SQLException {
Statement statement = connection.createStatement();
// 多线程环境下会出现并发问题
}
}
传统方式,要把它变成线程安全,必须使用锁,这会带来性能开销。
使用ThreadLocal
的优雅解决方案:
public class TopicDao {
// 方式1:重写initialValue方法
private static final ThreadLocal<Connection> connectionHolder =
new ThreadLocal<Connection>() {
@Override
protected Connection initialValue() {
try {
return DriverManager.getConnection(DB_URL, USER, PASS);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
};
// 方式2:使用Java 8的withInitial方法(推荐)
private static final ThreadLocal<Connection> connectionHolder2 =
ThreadLocal.withInitial(() -> {
try {
return DriverManager.getConnection(DB_URL, USER, PASS);
} catch (SQLException e) {
throw new RuntimeException(e);
}
});
public void addTopic() throws SQLException {
Connection conn = connectionHolder.get(); // 获取当前线程的连接
Statement statement = conn.createStatement();
// 每个线程使用自己的连接,无需同步
}
public void close() {
Connection conn = connectionHolder.get();
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
// log error
}
connectionHolder.remove(); // 重要:清理ThreadLocal
}
}
}
以这种方式运行,程序会为每个线程单独分配一个连接,避免了线程间的竞争。
# 二、典型应用场景
# 1、Web请求性能监控
在Spring中统计URL请求的执行时间,使用拦截器配合ThreadLocal
:
public class PerformanceInterceptor extends HandlerInterceptorAdapter {
private final Logger logger = LoggerFactory.getLogger(getClass());
private static final ThreadLocal<Long> TIME_THREAD_LOCAL = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) {
TIME_THREAD_LOCAL.set(System.currentTimeMillis());
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) {
Long startTime = TIME_THREAD_LOCAL.get();
if (startTime != null) {
long elapsed = System.currentTimeMillis() - startTime;
logger.info("{} 访问时间为{}ms", request.getRequestURL(), elapsed);
TIME_THREAD_LOCAL.remove(); // 必须清理,防止内存泄漏
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
// 确保清理,即使发生异常
TIME_THREAD_LOCAL.remove();
}
}
# 2、用户上下文传递
在多层架构中传递用户信息,避免在每个方法中传递参数:
public class UserContext {
private static final ThreadLocal<User> userHolder = new ThreadLocal<>();
public static void setUser(User user) {
userHolder.set(user);
}
public static User getUser() {
return userHolder.get();
}
public static void clear() {
userHolder.remove();
}
}
// 在过滤器中设置用户信息
public class AuthenticationFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
try {
User user = authenticate(request);
UserContext.setUser(user);
chain.doFilter(request, response);
} finally {
UserContext.clear(); // 请求结束时清理
}
}
}
// 在业务层直接获取用户信息
public class OrderService {
public void createOrder(Order order) {
User currentUser = UserContext.getUser(); // 无需通过参数传递
order.setUserId(currentUser.getId());
// ...
}
}
# 3、日期格式化工具
SimpleDateFormat
不是线程安全的,使用ThreadLocal
解决:
public class DateFormatUtil {
// SimpleDateFormat线程不安全,每个线程维护自己的实例
private static final ThreadLocal<SimpleDateFormat> dateFormatHolder =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static String format(Date date) {
return dateFormatHolder.get().format(date);
}
public static Date parse(String dateStr) throws ParseException {
return dateFormatHolder.get().parse(dateStr);
}
}
# 三、Spring框架中的ThreadLocal
应用
# 1、Spring事务管理
在Spring的事务管理中,广泛使用ThreadLocal
来实现事务的线程隔离。在数据源获取工具DataSourceUtils#doGetConnection
方法中:
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
Assert.notNull(dataSource, "No DataSource specified");
// 从ThreadLocal中获取当前线程的连接
ConnectionHolder conHolder = (ConnectionHolder)
TransactionSynchronizationManager.getResource(dataSource);
if (conHolder != null && (conHolder.hasConnection() ||
conHolder.isSynchronizedWithTransaction())) {
conHolder.requested();
if (!conHolder.hasConnection()) {
logger.debug("Fetching resumed JDBC Connection from DataSource");
conHolder.setConnection(fetchConnection(dataSource));
}
return conHolder.getConnection();
}
// ... 创建新连接的逻辑
return con;
}
TransactionSynchronizationManager
类使用多个ThreadLocal
变量来管理事务状态:
public abstract class TransactionSynchronizationManager {
// 存储事务资源(如数据库连接)
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
// 事务同步回调
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
// 当前事务名称
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<>("Current transaction name");
// 事务是否只读
private static final ThreadLocal<Boolean> currentTransactionReadOnly =
new NamedThreadLocal<>("Current transaction read-only status");
// 事务隔离级别
private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
new NamedThreadLocal<>("Current transaction isolation level");
// 事务是否激活
private static final ThreadLocal<Boolean> actualTransactionActive =
new NamedThreadLocal<>("Actual transaction active");
}
这种设计使得Spring能够在同一个应用中支持多个并发事务,每个线程维护自己的事务状态,互不干扰。
# 2、Security上下文
Spring Security使用SecurityContextHolder
通过ThreadLocal
存储安全上下文:
public class SecurityContextHolder {
private static ThreadLocalSecurityContextHolderStrategy strategy =
new ThreadLocalSecurityContextHolderStrategy();
private static final class ThreadLocalSecurityContextHolderStrategy
implements SecurityContextHolderStrategy {
private static final ThreadLocal<SecurityContext> contextHolder =
new ThreadLocal<>();
public SecurityContext getContext() {
SecurityContext ctx = contextHolder.get();
if (ctx == null) {
ctx = createEmptyContext();
contextHolder.set(ctx);
}
return ctx;
}
}
}
# 四、内存泄漏问题与最佳实践
# 1、内存泄漏原因分析
ThreadLocal
可能导致内存泄漏,主要原因是:
- 弱引用机制:
ThreadLocalMap
中的Entry
继承自WeakReference<ThreadLocal>
,key是弱引用,但value是强引用 - 线程池场景:线程池中的线程会被重复使用,如果不清理
ThreadLocal
,会导致value无法被回收
// 内存泄漏示例
public class MemoryLeakDemo {
// 线程池复用线程
private static final ExecutorService executor = Executors.newFixedThreadPool(10);
public static void processBatch() {
ThreadLocal<byte[]> localStorage = new ThreadLocal<>();
executor.execute(() -> {
// 分配大对象
localStorage.set(new byte[1024 * 1024]); // 1MB
// 业务处理...
// 忘记调用remove(),线程被复用时内存无法释放
// localStorage.remove(); // 必须调用!
});
}
}
# 2、最佳实践
# 2.1、使用try-finally确保清理
public class ThreadLocalBestPractice {
private static final ThreadLocal<UserInfo> userContext = new ThreadLocal<>();
public void process() {
try {
userContext.set(new UserInfo());
// 业务逻辑
doBusinessLogic();
} finally {
// 确保清理,防止内存泄漏
userContext.remove();
}
}
}
# 2.2、使用AutoCloseable自动清理
public class ThreadLocalResource implements AutoCloseable {
private final ThreadLocal<Resource> resourceHolder;
public ThreadLocalResource(Resource resource) {
this.resourceHolder = new ThreadLocal<>();
this.resourceHolder.set(resource);
}
public Resource get() {
return resourceHolder.get();
}
@Override
public void close() {
resourceHolder.remove();
}
}
// 使用示例
try (ThreadLocalResource resource = new ThreadLocalResource(new Resource())) {
// 使用资源
resource.get().doSomething();
} // 自动调用close()清理
# 2.3、静态变量vs实例变量
// 推荐:使用static final
public class GoodPractice {
private static final ThreadLocal<DateFormat> formatter =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
}
// 避免:实例变量可能导致ThreadLocal对象过多
public class BadPractice {
private ThreadLocal<DateFormat> formatter = new ThreadLocal<>(); // 每个实例一个
}
# 五、性能分析
# 1、时间复杂度
get()
操作:O(1) 平均情况,最坏O(n)(哈希冲突时线性探测)set()
操作:O(1) 平均情况remove()
操作:O(1) 平均情况
# 2、空间复杂度
- 每个线程维护独立的
ThreadLocalMap
- 空间占用 = 线程数 × 每个线程存储的对象大小
# 3、性能对比测试
public class PerformanceComparison {
private static final int THREAD_COUNT = 100;
private static final int OPERATIONS = 1000000;
// 同步方式
private static final Object lock = new Object();
private static int sharedCounter = 0;
// ThreadLocal方式
private static final ThreadLocal<Integer> localCounter =
ThreadLocal.withInitial(() -> 0);
@Test
public void testSynchronized() {
long start = System.currentTimeMillis();
// 使用synchronized
synchronized(lock) {
for (int i = 0; i < OPERATIONS; i++) {
sharedCounter++;
}
}
System.out.println("Synchronized: " + (System.currentTimeMillis() - start) + "ms");
}
@Test
public void testThreadLocal() {
long start = System.currentTimeMillis();
// 使用ThreadLocal
for (int i = 0; i < OPERATIONS; i++) {
localCounter.set(localCounter.get() + 1);
}
System.out.println("ThreadLocal: " + (System.currentTimeMillis() - start) + "ms");
}
}
# 六、适用场景与限制
# 1、适用场景
- 线程间数据隔离:每个线程需要独立的数据副本
- 跨层传递数据:避免在方法间传递大量参数
- 性能优化:避免同步开销,提高并发性能
- 线程不安全对象复用:如
SimpleDateFormat
、Random
等
# 2、不适用场景
- 父子线程间共享:
ThreadLocal
不支持继承(可用InheritableThreadLocal
) - 线程间通信:需要线程间共享数据时
- 大对象存储:会导致内存占用过高
- 短生命周期对象:频繁创建销毁会带来额外开销
# 七、总结
ThreadLocal
是Java并发编程中的重要工具,通过为每个线程维护独立的变量副本,实现了线程安全的同时避免了同步开销。它在Spring框架、Web应用、数据库连接管理等场景中有广泛应用。
关键要点:
- 原理:每个线程拥有自己的
ThreadLocalMap
,实现数据隔离 - 性能:以空间换时间,避免锁竞争
- 注意事项:必须及时调用
remove()
方法,防止内存泄漏 - 最佳实践:使用
try-finally
或AutoCloseable
确保资源清理
掌握ThreadLocal
的正确使用方法,能够帮助我们编写出更高效、更优雅的并发程序。
祝你变得更强!