Giá trị của ThreadLocal không nằm ở đối tượng ThreadLocal, mà nằm trong ThreadLocalMap thuộc về chính thread. Vòng đời giá trị vì thế bằng vòng đời thread.
Với thread thường, thread chết là map biến mất. Với thread pool thì thread không bao giờ chết — nó xử lý xong request rồi quay lại pool, mang theo mọi giá trị đã set.
Hai hậu quả:
1. Rò rỉ bộ nhớ — giá trị (user context, entity, byte buffer) bị worker thread giữ vô thời hạn. Với 200 worker, đó là 200 object không bao giờ được thu hồi, tích tụ tới OutOfMemoryError.
2. Rò rỉ dữ liệu giữa request — request sau nhận trúng thread cũ và đọc được context của user trước. Đây là lỗi bảo mật thật, không chỉ là vấn đề bộ nhớ.
Cách đúng — luôn dọn trong finally:
private static final ThreadLocal<UserContext> CTX = new ThreadLocal<>();
try {
CTX.set(ctx);
handle(request);
} finally {
CTX.remove(); // bắt buộc, kể cả khi ném exception
}Hai chi tiết hay bị hỏi thêm:
- set(null) không đủ. Nó chỉ đặt value về null nhưng entry vẫn nằm trong map; remove() mới xoá cả entry.
- Key trong ThreadLocalMap là weak reference tới đối tượng ThreadLocal, nhưng value là strong reference. Nên key có thể bị thu hồi, để lại entry key null mà value vẫn sống — JDK chỉ dọn các entry này một cách cơ hội khi map được truy cập, không đảm bảo.
- Nếu value giữ tham chiếu tới class của ứng dụng, cả classloader bị pin theo → OutOfMemoryError: Metaspace sau vài lần redeploy.