Что такое ThreadLocal и когда его использовать?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
ThreadLocal: назначение и использование
ThreadLocal — это класс в Java, который предоставляет хранилище данных, изолированное для каждого потока. Каждый поток может иметь собственное значение переменной, не влияя на другие потоки.
Что такое ThreadLocal
ThreadLocal создает отдельное значение переменной для каждого потока. Если у тебя есть глобальная переменная, то разные потоки конкурируют за нее. ThreadLocal позволяет каждому потоку иметь собственную копию переменной.
Проблема: Race Condition без ThreadLocal
public class UnsafeUserContext {
private static User currentUser; // Общая переменная для всех потоков — ОПАСНО!
public void setCurrentUser(User user) {
currentUser = user; // Один поток перезаписывает значение другого
}
public User getCurrentUser() {
return currentUser;
}
}
// Проблема:
public class RequestHandler implements Runnable {
private String username;
public RequestHandler(String username) {
this.username = username;
}
@Override
public void run() {
User user = new User(username);
context.setCurrentUser(user); // Поток 1 устанавливает пользователя
try {
Thread.sleep(100); // Имитируем обработку
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
User retrieved = context.getCurrentUser();
System.out.println(Thread.currentThread().getName() + ": " + retrieved.getUsername());
// Может быть не совпадение! Вывод: Потом 1 установил "Alice", но получил "Bob" (от потока 2)
}
}
Решение: ThreadLocal
public class SafeUserContext {
// Каждый поток имеет собственное значение
private static final ThreadLocal<User> userContext = new ThreadLocal<>();
public void setCurrentUser(User user) {
userContext.set(user); // Установить значение для текущего потока
}
public User getCurrentUser() {
return userContext.get(); // Получить значение для текущего потока
}
public void clear() {
userContext.remove(); // ВАЖНО: удалить значение (очистить память)
}
}
public class SafeRequestHandler implements Runnable {
private String username;
private SafeUserContext context;
public SafeRequestHandler(String username, SafeUserContext context) {
this.username = username;
this.context = context;
}
@Override
public void run() {
try {
User user = new User(username);
context.setCurrentUser(user);
Thread.sleep(100); // Имитируем обработку
User retrieved = context.getCurrentUser();
System.out.println(
Thread.currentThread().getName() + ": " +
retrieved.getUsername()
);
// Результат: Поток 1 получит "Alice", Поток 2 получит "Bob" — ПРАВИЛЬНО
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
context.clear(); // Очистить для предотвращения утечек памяти
}
}
}
public class ThreadLocalExample {
public static void main(String[] args) throws InterruptedException {
SafeUserContext context = new SafeUserContext();
// Запускаем несколько потоков
Thread thread1 = new Thread(new SafeRequestHandler("Alice", context), "Thread-1");
Thread thread2 = new Thread(new SafeRequestHandler("Bob", context), "Thread-2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
// Вывод:
// Thread-1: Alice
// Thread-2: Bob
}
}
Методы ThreadLocal
set(T value) — установить значение для текущего потока:
ThreadLocal<String> name = new ThreadLocal<>();
name.set("John"); // Установить для текущего потока
get() — получить значение текущего потока:
String value = name.get(); // Получить для текущего потока
remove() — удалить значение текущего потока:
name.remove(); // Удалить из памяти
initialValue() — установить начальное значение (перезаписать метод):
ThreadLocal<SimpleDateFormat> dateFormat = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
Практические примеры использования ThreadLocal
1. Web фреймворк (Spring, Servlet)
public class RequestContextHolder {
private static final ThreadLocal<HttpRequest> requestContext = new ThreadLocal<>();
private static final ThreadLocal<User> userContext = new ThreadLocal<>();
public static void setRequest(HttpRequest request) {
requestContext.set(request);
}
public static HttpRequest getRequest() {
return requestContext.get();
}
public static void setUser(User user) {
userContext.set(user);
}
public static User getUser() {
return userContext.get();
}
public static void clear() {
requestContext.remove();
userContext.remove();
}
}
@Service
public class UserService {
public User getCurrentUser() {
return RequestContextHolder.getUser(); // Получить текущего пользователя
}
public void updateUser(User user) {
User current = getCurrentUser();
// Использование context-aware логики
}
}
// Filter (Servlet)
public class AuthenticationFilter implements Filter {
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain
) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
try {
// Установить user в ThreadLocal
User user = extractUser(httpRequest);
RequestContextHolder.setUser(user);
RequestContextHolder.setRequest(httpRequest);
// Процесс запроса
chain.doFilter(request, response);
} finally {
// Очистить ThreadLocal
RequestContextHolder.clear();
}
}
private User extractUser(HttpServletRequest request) {
// Извлечь user из JWT токена
return null; // Упрощено
}
}
2. Логирование с корреляционным ID
public class LogContext {
private static final ThreadLocal<String> correlationId = new ThreadLocal<>();
public static void setCorrelationId(String id) {
correlationId.set(id);
}
public static String getCorrelationId() {
return correlationId.get();
}
public static void clear() {
correlationId.remove();
}
}
public class CorrelationIdFilter implements Filter {
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain
) throws IOException, ServletException {
String correlationId = UUID.randomUUID().toString();
LogContext.setCorrelationId(correlationId);
try {
chain.doFilter(request, response);
} finally {
LogContext.clear();
}
}
}
public class RequestLogger {
private static final Logger logger = LoggerFactory.getLogger(RequestLogger.class);
public void logRequest(String message) {
String correlationId = LogContext.getCorrelationId();
logger.info("[{}] {}", correlationId, message);
// Вывод: [uuid-123] User login attempt
}
}
3. Transaction Management
public class TransactionManager {
private static final ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();
public static void startTransaction() {
try {
Connection connection = DriverManager.getConnection("jdbc:...");
connection.setAutoCommit(false);
connectionHolder.set(connection);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public static Connection getConnection() {
return connectionHolder.get();
}
public static void commit() {
try {
Connection connection = getConnection();
if (connection != null) {
connection.commit();
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public static void rollback() {
try {
Connection connection = getConnection();
if (connection != null) {
connection.rollback();
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public static void cleanup() {
Connection connection = connectionHolder.get();
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
connectionHolder.remove();
}
}
ВАЖНО: Утечки памяти с ThreadLocal
Проблема: если не вызвать remove(), объект остается в памяти потока и не может быть собран garbage collector-ом.
// ❌ ПЛОХО: утечка памяти
public class BadThreadLocalUsage {
private static final ThreadLocal<User> user = new ThreadLocal<>();
public void processRequest(User currentUser) {
user.set(currentUser);
// Забыли вызвать remove() — утечка памяти!
}
}
// ✅ ХОРОШО: правильное использование
public class GoodThreadLocalUsage {
private static final ThreadLocal<User> user = new ThreadLocal<>();
public void processRequest(User currentUser) {
try {
user.set(currentUser);
// обработка
} finally {
user.remove(); // Всегда очищать в finally блоке
}
}
}
ThreadLocal в Thread Pool
Особая осторожность при использовании потоков из пула (ExecutorService):
public class ThreadPoolThreadLocalProblem {
private static final ThreadLocal<String> context = new ThreadLocal<>();
private static final ExecutorService executor = Executors.newFixedThreadPool(2);
public static void main(String[] args) {
// Поток 1 из пула
executor.submit(() -> {
context.set("Request 1");
System.out.println("Task 1: " + context.get());
// Забыли remove()!
});
// Поток 2 из пула (может переиспользовать поток 1)
executor.submit(() -> {
System.out.println("Task 2: " + context.get());
// Вывод: "Request 1" — ОШИБКА! Получили данные от предыдущей задачи
});
}
}
// ✅ Решение: всегда очищать
public class ThreadPoolThreadLocalFix {
private static final ThreadLocal<String> context = new ThreadLocal<>();
public static void main(String[] args) {
executor.submit(() -> {
try {
context.set("Request 1");
// обработка
} finally {
context.remove(); // КРИТИЧНО!
}
});
}
}
Когда использовать ThreadLocal
✅ Используй ThreadLocal для:
- Context информации в многопоточном приложении (текущий пользователь, request ID)
- Управления ресурсами (connection, transaction)
- Логирования с контекстом (MDC в Log4j)
- Временных данных, специфичных для потока
❌ НЕ используй ThreadLocal для:
- Общих данных между потоками
- Простого синхронизированного хранилища (используй synchronized или ConcurrentHashMap)
- Больших объемов данных
Альтернативы ThreadLocal
1. Dependency Injection (современный подход в Spring)
@Service
public class UserService {
@Autowired
private SecurityContext securityContext; // Управляется Spring
public User getCurrentUser() {
return securityContext.getUser();
}
}
2. Явная передача параметров
public class ExplicitContext {
public void processRequest(User user, String correlationId) {
handleUser(user, correlationId);
}
private void handleUser(User user, String correlationId) {
// Явно передаем контекст
}
}
Вывод
ThreadLocal — это мощный инструмент для управления контекстом в многопоточных приложениях. Основные правила:
- Всегда вызывай remove() в finally блоке
- Будь осторожен с thread pools — переиспользование потоков
- Используй ThreadLocal для контекста, а не для обмена данными между потоками
- В современной Java предпочитай Dependency Injection (Spring)
- Помни о утечках памяти — ThreadLocal может быть источником проблем в production