1. Vì sao ThreadLocal dần mất đi tính phù hợp
ThreadLocal dùng để làm gì?
Trong mô hình đa luồng cổ điển, nơi các luồng sống lâu (ví dụ trên máy chủ), đôi khi cần lưu trữ dữ liệu riêng cho từng luồng — những dữ liệu không được trộn lẫn với luồng khác. Chẳng hạn, tên người dùng, ID yêu cầu hoặc bộ đệm tạm.
Để làm điều đó, Java có ThreadLocal<T> — một “không gian riêng” của luồng, nơi có thể giữ dữ liệu mà không ảnh hưởng tới “hàng xóm”:
ThreadLocal<String> user = new ThreadLocal<>();
user.set("Alice"); // giá trị chỉ được lưu cho chính luồng này
String name = user.get(); // sẽ trả về "Alice" tại đây; ở các luồng khác — null
Vì sao ThreadLocal không hợp với luồng ảo
Luồng ảo sống khác hẳn so với các luồng “nặng” cũ. Chúng xuất hiện và biến mất hàng nghìn lần — đôi khi chỉ trong phần nhỏ của mili-giây. Còn ThreadLocal lại gắn dữ liệu với một luồng cụ thể, như thể nó sống mãi.
Khi một luồng ảo kết thúc, dữ liệu của nó trong ThreadLocal có thể vẫn treo trong bộ nhớ — dù bản thân luồng đã chết từ lâu. Điều này dẫn đến rò rỉ bộ nhớ, vì JVM không phải lúc nào cũng biết rằng các giá trị đó không còn cần thiết.
Nếu luồng được tái sử dụng (ví dụ trong pool), còn có tình huống khó chịu hơn: ngữ cảnh “người khác” có thể vô tình chuyển sang yêu cầu mới. Hãy tưởng tượng, người dùng Petya nhận dữ liệu của Vasya — và xin chào, bug cùng lỗ hổng.
ThreadLocal rất phù hợp nơi số luồng ít và sống lâu. Nhưng với luồng ảo — đó như việc cất đồ vào cái tủ biến mất mỗi giây.
2. Scoped Values: cách mới để truyền ngữ cảnh
Scoped Values là công cụ mới trong Java 21, giải quyết vấn đề cũ của ThreadLocal theo cách tinh gọn. Thay vì giữ dữ liệu bên trong luồng như ThreadLocal, nó “gắn” dữ liệu vào phạm vi thực thi — tức là một đoạn mã cụ thể. Giá trị chỉ tồn tại khi đoạn mã đó chạy và sau đó tự động biến mất, không để lại dấu vết trong bộ nhớ.
import java.lang.ScopedValue;
ScopedValue<String> USER = ScopedValue.newInstance();
ScopedValue.where(USER, "Alice").run(() -> {
System.out.println("Hello, " + USER.get()); // Sẽ in: Hello, Alice
});
Khi mã thoát khỏi khối run, giá trị không còn khả dụng — cố gắng truy cập sẽ ném ngoại lệ. Không cần dọn thủ công.
Scoped Values không làm bẩn bộ nhớ, không gây nhầm lẫn ngữ cảnh giữa các luồng và cho phép tạo phạm vi lồng nhau, nơi giá trị bên trong tạm thời ghi đè giá trị bên ngoài. Đây là cách truyền ngữ cảnh gọn gàng, đoán định và an toàn, đặc biệt trong thế giới luồng ảo.
3. Ví dụ sử dụng Scoped Values
Ví dụ 1: Truyền ngữ cảnh người dùng
Giả sử chúng ta có máy chủ xử lý yêu cầu từ nhiều người dùng khác nhau. Với mỗi yêu cầu, ta muốn biết ai là người khởi tạo.
import java.lang.ScopedValue;
public class ServerExample {
static final ScopedValue<String> USER = ScopedValue.newInstance();
public static void main(String[] args) {
processRequest("Alice");
processRequest("Bob");
}
static void processRequest(String userName) {
ScopedValue.where(USER, userName).run(() -> {
handleBusinessLogic();
});
}
static void handleBusinessLogic() {
System.out.println("Xử lý cho người dùng: " + USER.get());
}
}
Điều gì xảy ra:
- Mỗi yêu cầu có một scope riêng, trong đó USER bằng “Alice” hoặc “Bob”.
- Bên trong handleBusinessLogic() ta luôn nhận đúng tên người dùng.
- Ngay khi xử lý xong yêu cầu, giá trị biến mất.
Ví dụ 2: Ghi log kèm ngữ cảnh
Giả sử chúng ta muốn tự động chèn ID yêu cầu vào log:
import java.lang.ScopedValue;
public class LoggingExample {
static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
public static void main(String[] args) {
for (int i = 1; i <= 3; i++) {
String reqId = "REQ-" + i;
ScopedValue.where(REQUEST_ID, reqId).run(() -> {
log("Bắt đầu xử lý");
doWork();
log("Kết thúc xử lý");
});
}
}
static void log(String message) {
System.out.printf("[%s] %s%n", REQUEST_ID.get(), message);
}
static void doWork() {
log("Đang làm việc...");
}
}
Kết quả (ví dụ):
[REQ-1] Bắt đầu xử lý
[REQ-1] Đang làm việc...
[REQ-1] Kết thúc xử lý
[REQ-2] Bắt đầu xử lý
[REQ-2] Đang làm việc...
[REQ-2] Kết thúc xử lý
[REQ-3] Bắt đầu xử lý
[REQ-3] Đang làm việc...
[REQ-3] Kết thúc xử lý
Mỗi scope giữ ID yêu cầu riêng; không có chuyện nhầm lẫn giữa các luồng.
4. Scoped Values và luồng ảo: cặp đôi hoàn hảo
Vì sao Scoped Values đặc biệt hữu ích với luồng ảo
Luồng ảo sống rất ngắn — được tạo và hủy hàng nghìn cái, đôi khi trong phần nhỏ của giây. Vì thế cách cũ với ThreadLocal, nơi dữ liệu “gắn chặt” vào chính luồng, đơn giản là không hiệu quả: luồng biến mất quá nhanh, còn ngữ cảnh có thể rò rỉ hoặc bị lẫn.
ScopedValue thì ngược lại, gắn dữ liệu với chính tác vụ — với phạm vi thực thi của nó. Điều đó có nghĩa ngữ cảnh (ví dụ tên người dùng hoặc ID yêu cầu) đi theo mã, không đi theo luồng. Khi tác vụ kết thúc, giá trị tự động biến mất. Với luồng ảo, đây là giải pháp lý tưởng: an toàn, gọn gàng và không bất ngờ.
Ví dụ: Xử lý hàng loạt tác vụ với luồng ảo
import java.lang.ScopedValue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class VirtualThreadScopedValueDemo {
static final ScopedValue<Integer> TASK_ID = ScopedValue.newInstance();
public static void main(String[] args) {
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 1; i <= 10_000; i++) {
int taskId = i;
executor.submit(() -> ScopedValue.where(TASK_ID, taskId).run(() -> {
processTask();
}));
}
executor.shutdown();
}
static void processTask() {
// Mỗi tác vụ có TASK_ID riêng
System.out.println("Đang xử lý tác vụ #" + TASK_ID.get());
}
}
Các điểm chính:
- Mỗi tác vụ được tạo một scope riêng cho giá trị TASK_ID.
- Ngay cả khi các tác vụ chạy song song, các giá trị không bị lẫn giữa các luồng.
- Không rò rỉ bộ nhớ: scope “chết” cùng với tác vụ.
5. So sánh: ThreadLocal vs ScopedValue
| Tiêu chí | ThreadLocal | ScopedValue |
|---|---|---|
| Ràng buộc | Với luồng | Với phạm vi mã (scope) |
| Vòng đời | Chừng nào luồng còn sống | Chừng nào scope đang chạy |
| An toàn | Nguy cơ rò rỉ, nhầm lẫn | Không rò rỉ, không nhầm lẫn |
| Luồng ảo | Kém hiệu quả, rủi ro | Rất phù hợp |
| Sử dụng | |
|
| Lồng nhau | Không hỗ trợ override | Có thể ghi đè giá trị |
6. Phạm vi lồng nhau (scopes): ghi đè giá trị
ScopedValue<String> INFO = ScopedValue.newInstance();
ScopedValue.where(INFO, "Bên ngoài").run(() -> {
System.out.println(INFO.get()); // "Bên ngoài"
ScopedValue.where(INFO, "Bên trong").run(() -> {
System.out.println(INFO.get()); // "Bên trong"
});
System.out.println(INFO.get()); // "Bên ngoài"
});
Kết quả:
Bên ngoài
Bên trong
Bên ngoài
Điều này hữu ích khi, ví dụ, bên trong một tác vụ cần tạm thời ghi đè giá trị ngữ cảnh.
Scoped Values: các kịch bản sử dụng điển hình
- Truyền ID người dùng hoặc yêu cầu: để ghi log hành động hoặc kiểm tra quyền.
- Ghi log: tự động chèn ngữ cảnh vào log.
- Trace: phục vụ debug và profiling.
- Tham số giao dịch: ví dụ mức độ cô lập hoặc chế độ hoạt động.
- Bất kỳ “ngữ cảnh” nào chỉ nhìn thấy trong phạm vi một tác vụ (hoặc các tác vụ con của nó).
7. Cơ chế mới khác: Structured Concurrency
Structured Concurrency là cách tiếp cận trong đó các tác vụ liên quan (ví dụ, tiến trình con của một thao tác) được quản lý như một tổng thể: nếu tác vụ cha kết thúc hoặc thất bại, mọi tác vụ con đều tự động bị hủy. Điều này giảm rủi ro các luồng “bị quên” hoặc “treo”.
Ví dụ (rất sơ lược):
try (var scope = StructuredTaskScope.ShutdownOnFailure()) {
Future<String> result1 = scope.fork(() -> fetchData1());
Future<String> result2 = scope.fork(() -> fetchData2());
scope.join(); // chờ cả hai hoàn thành
scope.throwIfFailed(); // nếu có cái nào thất bại — ném ngoại lệ
String combined = result1.resultNow() + result2.resultNow();
System.out.println(combined);
}
Ưu điểm:
- Quản lý vòng đời tác vụ sạch sẽ hơn.
- Không còn các tiến trình con “treo”.
- Dễ xử lý lỗi hơn.
Structured Concurrency hiện vẫn ở chế độ preview, nhưng đang được phát triển tích cực.
8. Mẹo thực tiễn và hạn chế
Khi nào nên dùng Scoped Values?
- Bất cứ khi nào cần truyền ngữ cảnh giữa các tác vụ, đặc biệt với luồng ảo.
- Nếu trước đây bạn dùng ThreadLocal — hãy cân nhắc chuyển sang ScopedValue.
Khi nào vẫn cần ThreadLocal?
- Trong một số ít trường hợp, khi một luồng sống rất lâu và ngữ cảnh phải “cố định” trong suốt vòng đời của nó (ví dụ khi làm việc với mã legacy).
Hạn chế
- Không thể thay đổi Scoped Values sau khi tạo scope — chúng “chỉ đọc”.
- Không thể dùng Scoped Values ngoài scope: cố đọc giá trị ngoài phạm vi sẽ gây ngoại lệ.
- Đừng dùng Scoped Values để giữ các đối tượng lớn — phạm vi nên nhẹ và nhanh.
9. Các lỗi thường gặp khi dùng Scoped Values
Lỗi số 1: cố lấy giá trị ngoài scope. Nếu gọi USER.get() ngoài khối ScopedValue.where(...), bạn sẽ nhận ngoại lệ NoSuchElementException. Hãy đảm bảo chỉ truy cập bên trong phạm vi.
Lỗi số 2: cố thay đổi giá trị trong scope. Scoped Values không phải là container có thể thay đổi. Nếu cần “ghi đè” tạm thời, hãy tạo scope lồng nhau.
Lỗi số 3: dùng ThreadLocal và ScopedValue cùng lúc. Không nên trộn hai cơ chế này trừ khi thật sự cần thiết — dễ dẫn tới nhầm lẫn và lỗi ngữ cảnh.
Lỗi số 4: quên bọc logic trong khối run(). Nếu bạn viết ScopedValue.where(USER, "Alice") mà không có .run(() -> { ... }), sẽ không có scope nào được tạo!
Lỗi số 5: cố dùng Scoped Value cho thông tin toàn cục sống lâu. Với các mục đích như vậy, tốt hơn dùng biến thông thường hoặc ThreadLocal (nếu hợp lý).
GO TO FULL VERSION