Java Internals & Concurrency/Confinement: Thread safety bằng cách không chia sẻ
7/47
Bài 7 / 47~20 phútConcurrency cơ bảnMiễn phí lượt xem

Confinement: Thread safety bằng cách không chia sẻ

Thread safety bằng cách triệt tính shared: ad-hoc confinement, stack confinement, ThreadLocal và ScopedValue (Java 25). Khi nào confinement là đủ.

TL;DR: Confinement triệt tiêu tính "shared": dữ liệu chỉ một thread chạm tới thì tự động thread-safe, kể cả khi class của nó không hề thread-safe. Bài này đi dọc một trục từ mong manh tới cứng cáp: ad-hoc confinement (chỉ dựa quy ước), stack confinement (biến cục bộ — ngôn ngữ bảo đảm, miễn tham chiếu không rò khỏi stack frame), ThreadLocal (mỗi thread một bản riêng trong ThreadLocalMap của chính Thread — nhưng trên thread pool, quên remove() là rò rỉ cả dữ liệu lẫn bộ nhớ), và ScopedValue (Java 25) — bound theo phạm vi, immutable, tự dọn. Né chia sẻ bao giờ cũng rẻ hơn đồng bộ hóa.

1. Ad-hoc confinement: khi chỉ có quy ước giữ dữ liệu

Bài trước khép lại bằng bốn chiến lược đối phó với shared mutable state: gốc của rắc rối nằm ở hai tính từ "shared" và "mutable". Lần này ta cắt tính từ thứ nhất. Dữ liệu chỉ thuộc về một thread duy nhất thì không còn "shared", và khi ấy atomicity lẫn visibility đều hết là vấn đề — confinement né hẳn câu hỏi mà khóa, happens-before và atomic class phải trả lời. Java không có từ khóa nào khai báo "biến này thuộc về một thread", nên bài đi dọc một trục: từ dạng chỉ có lời hứa của lập trình viên, tới dạng ngôn ngữ và runtime đứng ra bảo đảm.

Dạng yếu nhất là ad-hoc confinement: giam dữ liệu hoàn toàn bằng kỷ luật người viết. Bạn quyết định một đối tượng "chỉ được thread X dùng", viết comment nói thế, rồi tin người sau tôn trọng nó — compiler im lặng, test đơn luồng vẫn xanh, lỗi chỉ lộ dưới tải.

public class RequestProcessor {
    // Quy uoc (chi la comment): 'buffer' CHI duoc dung boi thread khoi tao processor.
    private final StringBuilder buffer = new StringBuilder();   // StringBuilder KHONG thread-safe
    public void append(String chunk) {
        buffer.append(chunk);     // an toan — neu, va chi neu, quy uoc tren duoc giu
    }
}

StringBuilder không thread-safe, nhưng nếu thật sự chỉ một thread gọi append thì đoạn này chạy đúng mãi mãi: đối tượng bị giam thì an toàn kể cả khi class của nó không thread-safe. Vấn đề nằm ở cụm "nếu thật sự" — quy ước không được ngôn ngữ thực thi, nên chỉ mạnh bằng người yếu nhất chạm vào code. Một getter công khai hay một lambda submit sang executor là đủ phá vỡ.

Dù vậy, một biến thể của ad-hoc đáng giá tới mức thành quyết định kiến trúc: giam cả một phân hệ vào một thread. Swing là ví dụ kinh điển — mọi thao tác lên UI component bị giam vào event dispatch thread, biến bài toán đồng bộ hóa khổng lồ thành một luật duy nhất. TicketFlow áp cùng tinh thần: dồn mọi yêu cầu đặt vé vào một hàng đợi cho đúng một thread tiêu thụ chạy tuần tự — eventssold bị giam trong thread đó, book không cần một dòng synchronized nào.

var requests = new LinkedBlockingQueue<BookingCommand>();
Thread worker = new Thread(() -> {
    var events = new HashMap<String, Event>();      // KHONG thread-safe -- va khong can thread-safe
    var sold   = new HashMap<String, Integer>();
    while (!Thread.currentThread().isInterrupted()) {
        try {
            requests.take().applyTo(events, sold);  // chi worker nay doc/ghi hai map tren
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();     // khoi phuc co interrupt -> vong lap thoat
        }
    }
});
worker.start();

LinkedBlockingQueue là một BlockingQueue — hàng đợi chặn: take() trên queue rỗng chờ tới khi có phần tử thay vì trả null ngay (bài Blocking queues & producer-consumer đào sâu họ cấu trúc này). Vì take() chờ vô hạn được nên nó ném InterruptedException; worker khôi phục cờ interrupt để vòng lặp thấy cờ và thoát êm thay vì nuốt mất tín hiệu dừng — pattern cooperative cancellation của bài Thread API và vòng đời.

Hai HashMap không thread-safe ở đây hoàn toàn ổn: tạo trong thân run, không bao giờ rò ra ngoài. Vẫn là ad-hoc confinement, nhưng gói trọn state vào một thread đã thu nhỏ bề mặt sai sót. BlockingQueue là thứ duy nhất được phép chia sẻ: đặt một đối tượng vào hàng đợi thread-safe sẽ safe-publish nó, worker thấy đối tượng hoàn chỉnh chứ không phải bản dở dang vì reordering (nền móng ở bài Immutability).

2. Stack confinement: vì sao biến cục bộ tự an toàn?

Nấc tiếp theo là stack confinement: đối tượng chỉ với tới được qua biến cục bộ. Ở đây ngôn ngữ đứng về phía ta — biến cục bộ sống trên stack, mà stack là trạng thái riêng của từng thread (bài Process và Thread).

Với kiểu nguyên thủy, giam là tuyệt đối: không có cách nào lấy tham chiếu tới một biến int cục bộ. Đó là lý do SeatPriceCalculator ở bài trước an toàn dù không một dòng đồng bộ hóa. Với kiểu tham chiếu thì phải cẩn thận: tham chiếu nằm trên stack, còn đối tượng nó trỏ tới nằm trên heap dùng chung — chỉ đúng chừng nào không có tham chiếu thứ hai lọt ra ngoài thread.

public Map<String, Long> countByTier(List<Booking> bookings) {
    Map<String, Long> counts = new HashMap<>();   // HashMap cuc bo — bi giam tren stack
    for (Booking b : bookings) counts.merge(b.tier(), 1L, Long::sum);
    return Map.copyOf(counts);                    // tra ra mot ban immutable, KHONG ro 'counts'
}

countsHashMap không thread-safe, nhưng được tạo, dùng và vứt bỏ trọn trong một lần gọi; mỗi thread có một counts riêng trên stack. Đối tượng thì mutable, cách dùng lại thread-safe — nhờ vậy ta dùng được tự do các collection rẻ, không đồng bộ, giữa một chương trình đa luồng.

Cái bẫy duy nhất là để tham chiếu thoát ra: return counts thay vì Map.copyOf(counts) là trao một tham chiếu sống cho caller. Tệ hơn là âm thầm để đối tượng cục bộ lọt vào cấu trúc sống lâu hơn stack frame — một listener, một field, hay một lambda submit vào executor:

public void process(List<Booking> bookings) {
    Map<String, Long> counts = new HashMap<>();          // tuong la bi giam...
    executor.submit(() -> counts.merge("VIP", 1L, Long::sum));  // ...nhung lambda mang no sang thread khac
    counts.merge("STANDARD", 1L, Long::sum);             // race: hai thread cung sua 'counts'
}

Khoảnh khắc counts bị lambda chạy trên thread khác bắt giữ, nó hết bị giam — dù tên biến vẫn là biến cục bộ. Ngôn ngữ giam phần tham chiếu trên stack, còn giữ đối tượng heap không rò ra ngoài là trách nhiệm người viết. Đổi lại, nó miễn phí về hiệu năng.

💡 Cách nhớ

Biến cục bộ kiểu nguyên thủy: giam tuyệt đối — không tồn tại cách lấy tham chiếu. Biến cục bộ kiểu tham chiếu: giam có điều kiện — object nằm trên heap, nên chỉ bị giam chừng nào không return tham chiếu sống, không gán vào field, không để lambda bắt giữ.

3. ThreadLocal

Đôi khi ta cần thứ vừa "toàn cục" trong tầm với của code, vừa riêng cho mỗi thread — điều mà ad-hoc lẫn stack confinement, vốn giam trong phạm vi hẹp, không cho được. ThreadLocal là cơ chế chính thức cho nhu cầu đó: hình dung nó như cái tủ nhiều ngăn, mỗi thread một ngăn mang tên mình, và thread A gọi get luôn nhận đúng giá trị chính A đã set.

Tủ nhiều ngănThreadLocal
Cái tủ giữa văn phòngobject ThreadLocal — điểm truy cập chung
Ngăn mang tên từng ngườientry trong ThreadLocalMap của từng Thread
Chìa khóa nhận diện chủ ngănThread.currentThread()
Đồ để quên trong ngăn khi nghỉ việcvalue chưa remove() khi thread quay về pool

Use case kinh điển là per-thread context: thứ không thread-safe nhưng tiện khi dùng như biến toàn cục suốt vòng đời của một thread. Ví dụ sách giáo khoa là SimpleDateFormat — thay vì khóa nó, ta phát cho mỗi thread một bản riêng:

public final class TicketDateFormat {
    private static final ThreadLocal<DateFormat> FORMAT =
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
    public static String format(Date date) {
        return FORMAT.get().format(date);   // moi thread dung SimpleDateFormat cua rieng no
    }
}

withInitial khai báo cách tạo giá trị ban đầu: lần đầu thread gọi get, lambda dựng riêng cho nó một SimpleDateFormat. Hai thread không bao giờ chạm cùng một formatter, nên cái không thread-safe trở nên an toàn mà không tốn khóa. Use case phổ biến khác trên server là giữ ngữ cảnh request — correlation ID cho log, user đã xác thực, hay một JDBC Connection (kết nối database, không thread-safe nên mỗi thread giữ một cái riêng) — cho tầng code sâu đọc mà không phải kéo qua từng tham số method.

3.1 Cơ chế bên dưới: ThreadLocalMap và WeakReference

Trực giác dễ tưởng ThreadLocal<T> là map từ thread sang giá trị, nằm trong object ThreadLocal. Thực tế ngược lại: dữ liệu nằm trong chính object Thread. Mỗi Thread mang field threadLocals kiểu ThreadLocal.ThreadLocalMap — hash map tí hon của riêng nó, key là các object ThreadLocal nó từng dùng. FORMAT.get() thực chất là mở threadLocals của Thread.currentThread() rồi tra bằng key FORMAT. Map đó chỉ thread chủ chạm tới, nên mọi get/set đều đơn luồng — không cần đồng bộ hóa. Dữ liệu đi theo thread.

Một chi tiết cài đặt quyết định câu chuyện rò rỉ: entry trong ThreadLocalMap giữ key qua WeakReference — tham chiếu yếu, không ngăn GC thu hồi object nó trỏ tới — còn value giữ qua strong reference. Nhờ vậy, khi object ThreadLocal không còn ai dùng, key bị GC thu và entry thành "stale entry", được dọn nhân tiện lúc thread gọi set/get/remove lần sau. Nhưng không gì bảo đảm việc dọn xảy ra, và value vẫn bị strong reference giữ chặt chừng nào entry còn trong map.

flowchart LR
    P["Thread pool"] --> T["Worker thread (song rat lau)"]
    T --> M["ThreadLocalMap"]
    M --> E["Entry"]
    E -.->|"weak ref (key)"| K["ThreadLocal"]
    E ==>|"strong ref (value)"| V["Value cua request cu"]
    style V fill:#FCA5A5
    style K fill:#6EE7B7

Thread còn sống thì map còn sống, entry chưa gỡ thì value còn bị trỏ tới — GC bó tay. Thread thường chết đi kéo map chết theo; thread trong pool không bao giờ chết.

3.2 Pitfall: ThreadLocal gặp thread pool

Thread trong pool tái sử dụng hết request này tới request khác. Một request đặt giá trị vào ThreadLocal rồi kết thúc: thread quay về pool mang nguyên giá trị cũ, và request sau chạy trên đúng thread đó thấy giá trị thừa — rò rỉ dữ liệu giữa các request. Nguy hiểm hơn là bộ nhớ: giá trị đó không bao giờ được thu hồi. Pool vài trăm thread, mỗi thread ôm một object nặng, là rò rỉ âm thầm.

// SAI: set ma khong remove -- thread quay ve pool van om gia tri cu
public class AuthFilter {
    private static final ThreadLocal<User> CURRENT_USER = new ThreadLocal<>();
    public void doFilter(Request req, Chain chain) {
        CURRENT_USER.set(authenticate(req));   // gan user cua request nay
        chain.proceed(req);                    // tang sau doc CURRENT_USER.get()
        // thieu remove(): request sau tren cung thread co the doc nham User cu
    }
}

Hai hậu quả cùng lúc. Về tính đúng đắn: request sau đi qua nhánh không gọi set thì CURRENT_USER.get() ở tầng sâu trả về user của request trước — lỗi lẫn ngữ cảnh mang màu sắc bảo mật. Về bộ nhớ: mỗi worker thread găm một User cùng mọi thứ nó trỏ tới, sống vô hạn.

Lời giải là kỷ luật remove: đặt gì vào ThreadLocal trong một tác vụ thì phải gỡ ra khi tác vụ kết thúc, an toàn nhất là trong khối finally.

// DUNG: remove trong finally -- don ngan truoc khi tra thread ve pool
private static final ThreadLocal<String> REQUEST_ID = new ThreadLocal<>();
public void handle(Request req) {
    REQUEST_ID.set(req.id());
    try {
        process(req);                 // cac tang sau doc REQUEST_ID.get()
    } finally {
        REQUEST_ID.remove();          // BAT BUOC tren thread pool -- neu khong, ro sang request sau
    }
}

finally bảo đảm việc gỡ xảy ra kể cả khi process ném exception. Bỏ qua remove không gây lỗi ngay, nó âm thầm tích tụ — và đó chính là điều khiến nó nguy hiểm. ThreadLocal mạnh về cô lập dữ liệu, nhưng đẩy sang ta trách nhiệm vòng đời: ai đặt thì người đó phải dọn.

4. InheritableThreadLocal

ThreadLocal cô lập triệt để — đôi khi quá triệt để: thread con mặc định khởi đầu với ngăn trống trơn. Đôi lúc ta muốn ngược lại, chẳng hạn để một tác vụ con vẫn gắn đúng correlation ID của request đã sinh ra nó. InheritableThreadLocal ra đời cho nhu cầu đó: tại thời điểm thread con được tạo, nó sao chép giá trị từ các InheritableThreadLocal của cha sang ngăn của mình.

private static final InheritableThreadLocal<String> TENANT = new InheritableThreadLocal<>();
TENANT.set("acme-corp");                            // cha dat gia tri truoc khi tao con
new Thread(() -> System.out.println(TENANT.get()))  // in "acme-corp" — con ke thua tu cha
    .start();

Bốn chữ "tại thời điểm tạo" mới là phần đáng giá: kế thừa chỉ là một lần chụp ảnh. Cha đổi giá trị sau đó thì con không thấy, và ngược lại. Vì là sao chép tham chiếu chứ không sao chép sâu, giá trị mutable khiến cha con cùng trỏ vào một đối tượng — chia sẻ trở lại. InheritableThreadLocal chỉ an toàn với giá trị immutable.

Giới hạn lộ ra ở thread pool và virtual thread. Kế thừa chỉ xảy ra khi một thread trực tiếp tạo thread khác; với executor, các worker đã dựng từ trước nên chẳng có "lúc tạo con" nào để chụp ảnh — ngữ cảnh không qua được ranh giới submit. Mô hình cha-con cũng không khớp với virtual thread tạo ra hàng loạt: mỗi lần spawn lại sao chép cả bản đồ ngữ cảnh là chi phí không nhỏ và một vòng đời mơ hồ.

5. ScopedValue (Java 25)

Điểm gãy ấy đặt nền cho ScopedValue, final từ Java 25. Thay vì gắn giá trị vào một thread rồi tin có ai đó sẽ dọn, nó ràng giá trị vào một phạm vi có biên giới rõ ràng, chỉ tồn tại trong lúc một khối code đang chạy. Nó immutable trong phạm vi ấy, không hề có set — khác biệt nền tảng so với ThreadLocal.

private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
public void handle(Request req) {
    ScopedValue.where(REQUEST_ID, req.id())
        .run(() -> process(req));    // REQUEST_ID.get() hop le o moi noi BEN TRONG khoi nay
    // ra khoi day, REQUEST_ID khong con bound — khong can, va khong the, remove()
}

Giá trị bound đúng cho khoảng thời gian khối lambda chạy, kể cả các lời gọi sâu bên trong, rồi tự unbound khi run trả về. Không có bước dọn thủ công nên cũng không có khả năng quên dọn — cửa sổ rò rỉ mà ThreadLocal mở ra trên thread pool bị đóng kín bằng chính cấu trúc API. Bất biến cũng loại bỏ lớp lỗi tầng code sâu lén set lại ngữ cảnh.

Khi một tầng sâu cần ngữ cảnh khác, nó không set đè, mà bind chồng một phạm vi con:

ScopedValue.where(REQUEST_ID, req.id()).run(() -> {
    audit();                                      // doc req.id()
    ScopedValue.where(REQUEST_ID, "system").run(() -> {
        cleanupExpiredHolds();                    // doc "system" -- chi trong khoi nay
    });
    audit();                                      // doc lai req.id() -- tu khoi phuc
});

Phạm vi con che giá trị của cha đúng trong khối của nó; khối kết thúc, giá trị cũ tự hiện lại — không cần trò lưu-rồi-khôi-phục String old = TL.get(); TL.set(x); ... TL.set(old) đầy rủi ro với ThreadLocal. Mọi "thay đổi" đều có biên giới thấy được trong cấu trúc code.

ScopedValue vì thế hợp với virtual thread — thứ được tạo ra rất nhiều (bài Process và Thread đã nói về mô hình một virtual thread cho mỗi request), nên cơ chế ngữ cảnh phải rẻ và sạch. Mô hình phạm vi có biên giới khớp tự nhiên với "một tác vụ, một phạm vi" của Structured Concurrency (vẫn preview ở Java 25), nơi tác vụ con kế thừa scoped value của cha mà không phải sao chép. Chi tiết ở bài Virtual threads và bài Structured Concurrency & ScopedValue.

6. Liên hệ các bài khác

  • Thread Safety — bài này thi công chiến lược thứ nhất trong bốn chiến lược bài đó gọi tên; thuật ngữ shared, mutable, race định nghĩa ở đó.
  • Thread API và vòng đời — pattern khôi phục cờ interrupt mà worker ở mục 1 dùng.
  • Immutability — dựng nền safe publication mà mục 1 nhắc lướt; cũng là chiến lược thứ hai, cắt vào tính từ còn lại.
  • Blocking queues & producer-consumer — đào sâu hàng đợi chặn làm cây cầu chia sẻ duy nhất giữa producer và worker bị giam.
  • Virtual threadsStructured Concurrency & ScopedValue — nơi ScopedValue phát huy trọn vẹn: ngữ cảnh kế thừa theo phạm vi.

7. 📚 Deep Dive Oracle

📚 Deep Dive Oracle

Spec / reference chính thức:

  • ThreadLocal Javadoc (Java 21) — contract chính thức, kể cả ghi chú về vòng đời giá trị gắn theo thread.
  • JEP 506 — Scoped Values (final, Java 25) — motivation section giải thích thẳng vì sao ThreadLocal không hợp với hàng triệu virtual thread.
  • Java Concurrency in Practice (Goetz et al.), §3.3 Thread Confinement — nguồn của trục ad-hoc → stack → ThreadLocal.

Ghi chú: JEP 506 đáng đọc nhất trong ba link — phần "Motivation" phân tích súc tích những điểm gãy của ThreadLocal mà bài này đã đi qua.

8. Tóm tắt

Confinement trả lời câu hỏi thread safety bằng cách từ chối đặt ra nó. Cái đáng mang theo là cái trục:

NấcAi đứng ra bảo đảmMong manh ở đâu
Ad-hocQuy ước + kỷ luật người viếtMột getter hay lambda phát tán tham chiếu là vỡ, compiler im lặng
StackNgôn ngữ — stack riêng từng threadĐối tượng heap rò khỏi stack frame
ThreadLocalRuntime — ThreadLocalMap trong từng ThreadQuên remove() trên thread pool là rò dữ liệu lẫn bộ nhớ
ScopedValueAPI — bound theo phạm vi, immutable, tự dọnPhải cấu trúc code theo scope; final từ Java 25

InheritableThreadLocal là nhánh phụ của nấc ba: chụp ảnh ngữ cảnh lúc tạo thread con, nhưng gãy ở thread pool và virtual thread.

Confinement là đủ khi ta thật sự có thể không chia sẻ. Nhưng có dữ liệu buộc phải đến tay nhiều thread: một Event mà hàng nghìn request cùng đọc. Với chúng ta không bỏ được tính "shared" — nhưng còn con dao thứ hai, cắt vào tính từ còn lại: dữ liệu chia sẻ mà không bao giờ thay đổi sau khi khởi tạo thì đọc lúc nào cũng cho cùng một kết quả. Đó là Immutability, bài tiếp theo.

9. Tự kiểm tra

Tự kiểm tra
Q1
Trong countByTier, một HashMap không thread-safe được dùng giữa chương trình đa luồng mà vẫn an toàn tuyệt đối. Vì sao? Điều gì sẽ phá vỡ bảo đảm đó?
Vì counts là biến cục bộ: tham chiếu nằm trên stack của thread đang gọi, và mỗi lần gọi method tạo một HashMap mới trên heap mà chỉ stack frame đó trỏ tới. Hai thread chạy đồng thời có hai counts hoàn toàn riêng — không chia sẻ thì không có gì để tranh. Bảo đảm vỡ ngay khi tham chiếu thoát khỏi stack frame: return thẳng counts thay vì bản copy, gán nó vào một field, đưa cho listener, hoặc để một lambda bắt giữ nó rồi submit sang executor — từ khoảnh khắc đó object heap bị nhiều thread với tới và stack confinement tan biến.
Q2
Trong worker single-threaded của TicketFlow, vì sao requests.take() phải bọc try/catch InterruptedException, và vì sao trong catch lại gọi Thread.currentThread().interrupt()?
take() là blocking call — nó có thể chờ vô hạn trên queue rỗng, nên phải có đường thoát khi ai đó muốn dừng thread: đó là InterruptedException, một checked exception bắt buộc xử lý. Khi exception được ném, JVM đồng thời xoá cờ interrupt của thread. Nếu chỉ nuốt exception, tín hiệu dừng biến mất và vòng lặp while chạy tiếp như chưa có gì. Gọi Thread.currentThread().interrupt() khôi phục cờ, để điều kiện !Thread.currentThread().isInterrupted() nhìn thấy và thoát êm — đúng pattern cooperative cancellation của bài Thread API và vòng đời.
Q3
ThreadLocal.get() tra cứu dữ liệu ở đâu? Vì sao thao tác đó không cần một chút đồng bộ hóa nào?

Dữ liệu không nằm trong object ThreadLocal mà nằm trong chính object Thread: mỗi thread mang một field threadLocals kiểu ThreadLocal.ThreadLocalMap, trong đó key là các object ThreadLocal thread từng dùng, value là giá trị của riêng thread đó. get() thực chất là: lấy Thread.currentThread(), mở map của thread ấy, tra bằng key là chính object ThreadLocal.

Không cần đồng bộ hóa vì map đó chỉ đúng một thread — thread chủ — đọc và ghi. Không có truy cập từ thread thứ hai thì không có data race; đây là confinement theo đúng nghĩa đen, dữ liệu đi theo thread.

Q4
Giải thích chuỗi tham chiếu gây memory leak khi dùng ThreadLocal với thread pool. Key được giữ bằng WeakReference — vì sao điều đó vẫn không cứu được value?
Chuỗi giữ sống là: thread pool giữ worker thread, thread giữ ThreadLocalMap, map giữ entry, entry giữ value bằng strong reference. Thread trong pool không bao giờ chết nên cả chuỗi sống vô hạn — GC không thể thu hồi value dù request đã xong từ lâu. WeakReference chỉ áp cho key (object ThreadLocal): khi không ai trỏ tới ThreadLocal nữa, key bị thu và entry thành stale entry, được dọn nhân tiện khi thread gọi set/get/remove lần sau — nhưng không có gì bảo đảm việc dọn xảy ra, và value vẫn bị strong reference giữ chặt cho tới lúc đó. Cách duy nhất chắc chắn là tự gỡ: remove() trong finally.
Q5
Vì sao InheritableThreadLocal không truyền được ngữ cảnh qua thread pool, và vì sao mô hình của nó không hợp với virtual thread?
Sự kế thừa của InheritableThreadLocal chỉ xảy ra tại đúng một thời điểm: lúc thread con được tạo, giá trị của cha được chụp sang map của con. Với thread pool, các worker đã được dựng từ trước, độc lập với thread đang submit tác vụ — không tồn tại khoảnh khắc cha-tạo-con nào để chụp, nên ngữ cảnh không qua được ranh giới submit. Với virtual thread, vấn đề là quy mô: chúng được tạo hàng loạt với số lượng khổng lồ, và việc mỗi lần spawn lại sao chép nguyên một bản đồ ngữ cảnh vừa tốn kém vừa mơ hồ về vòng đời. Đó chính là khoảng trống mà ScopedValue được thiết kế để lấp.
Q6
ScopedValue đóng những lỗ hổng nào của ThreadLocal, và đóng bằng cơ chế gì?
Ba lỗ hổng. Thứ nhất, quên dọn: ThreadLocal cần remove() thủ công trong finally, còn ScopedValue ràng giá trị vào một phạm vi where(...).run(...) — tự unbound khi khối kết thúc, không tồn tại API để quên. Thứ hai, mutability không kiểm soát: ThreadLocal cho phép mọi tầng code set lại giữa chừng, còn ScopedValue immutable trong suốt phạm vi, loại cả lớp lỗi một tầng sâu lén đổi ngữ cảnh. Thứ ba, chi phí kế thừa: thay vì sao chép bản đồ ngữ cảnh mỗi lần spawn thread, các tác vụ con trong Structured Concurrency kế thừa scoped value của cha một cách rõ ràng và rẻ. Đổi lại, nó đòi cấu trúc code theo phạm vi — không gắn tuỳ tiện vào vòng đời thread được nữa.

Bài tiếp theo: Immutability — thread safety bằng cách không thay đổi

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên