Java — Từ Zero đến Senior/synchronized và volatile — memory model và happens-before
~28 phútConcurrency cơ bảnMiễn phí

synchronized và volatile — memory model và happens-before

Atomicity vs visibility, synchronized là mutex + visibility, volatile chỉ visibility, khái niệm happens-before. Vì sao counter++ không safe dù thêm volatile, và làm cách nào JVM/CPU cache biến khiến thread A không thấy ghi của thread B.

Đoạn code sau trông vô hại:

class Counter {
    int value = 0;
    void increment() { value++; }
}

Khởi chạy 2 thread, mỗi thread gọi increment() 1 triệu lần. Sau khi cả hai xong, kỳ vọng value == 2_000_000.

Thực tế: chạy 10 lần, kết quả có thể là 1_234_567, 1_877_001, 1_543_281... khác nhau mỗi lần. Mất update silent. Không exception, không warning, compiler không báo. Chỉ là con số sai.

Điều gì xảy ra? Hai vấn đề đan xen:

  1. Atomicity: value++ không phải 1 thao tác — nó là 3 bước (read, +1, write). Thread A đọc 100, chưa kịp write, thread B cũng đọc 100, +1 ghi 101. A tiếp tục ghi 101. Kết quả 101 thay vì 102. Mất 1 update.

  2. Visibility: kể cả không chen giữa, thread A ghi value = 100 nhưng thread B có thể không thấy — vì CPU cache giá trị trong register, JVM optimize không đọc lại memory.

Bài này giải thích 2 vấn đề trên ở level CPU/JVM, sau đó 2 công cụ Java cung cấp: synchronized (mutex + visibility) và volatile (chỉ visibility). Kết thúc với happens-before — luật chính thức định nghĩa "thread nào thấy update của thread nào" trong JLS §17.

Đây là bài khó nhất module 10 nhưng quan trọng nhất. Hiểu sai memory model → code race condition chạy OK 99% thời gian, hỏng 1% lúc bất ngờ.

1. Analogy — Sổ ghi chép chung trong phòng

Hai nhân viên phải cập nhật một con số trong sổ kế toán chung.

Kịch bản 1 — không khoá, không broadcast: Cả hai đọc sổ thấy số 100, mỗi người ghi vào nháp riêng, cộng 1 thành 101, viết lại vào sổ. Cả hai ghi 101 — mất 1 update. Đây là atomicity: thao tác "đọc-tính-ghi" bị xen kẽ.

Thêm một bẫy: mỗi nhân viên có bản copy số đó trong đầu (CPU cache). A vừa ghi 101 vào sổ, B không nhìn sổ mà dùng số trong đầu (vẫn là 100) → ghi 101. Đây là visibility: update của A không lan tới B kịp.

Kịch bản 2 — cửa phòng có khoá: chỉ 1 người vào phòng tại 1 lúc. A vào, làm xong, đi ra. B vào, thấy số đã 101, cộng 1 thành 102. Đúng. Đây là synchronized — mutex + ép reload từ sổ mỗi lần vào.

Kịch bản 3 — không khoá nhưng ép mỗi lần đọc/ghi đi thẳng sổ: không ai dùng nháp, không ai cache trong đầu. Visibility đảm bảo. Nhưng 2 người vẫn có thể cùng lúc đọc, cùng lúc ghi — vẫn mất update. Đây là volatile — visibility mà không mutex.

Rút ra:

  • Cần visibility → volatile đủ.
  • Cần atomic (thao tác không bị chia cắt) → phải synchronized hoặc AtomicInteger.
  • volatile không thay thế được synchronized cho compound operation.

2. Hai vấn đề chi tiết — atomicity và visibility

Vấn đề 1: Atomicity

value++ compile ra 3 bytecode:

getfield value    // doc value tu heap vao stack
iconst_1          // push 1 vao stack
iadd              // cong 2 gia tri
putfield value    // ghi ket qua ve heap

Giữa getfieldputfield, thread khác có thể chen vào. Minh hoạ 2 thread tăng từ 0:

Thread A: getfield -> 0
Thread B: getfield -> 0          (A chua ghi, van la 0)
Thread B: iadd    -> 1
Thread B: putfield 1              (heap = 1)
Thread A: iadd    -> 1            (A van nho gia tri cu la 0, +1 = 1)
Thread A: putfield 1              (heap = 1, dang le la 2)

Kết quả: 2 lần increment, value chỉ tăng 1. Lỗi này gọi là lost update.

Tương tự với mọi compound op:

  • if (x == 0) x = 1 (check-then-act).
  • x = x * 2 (read-modify-write).
  • if (map.containsKey(k)) return map.get(k); else return null; (check-then-get, với concurrent modification có thể miss).

Vấn đề 2: Visibility

Ngay cả khi atomic (ví dụ chỉ có 1 thread ghi), visibility vẫn là vấn đề riêng.

class BadShutdown {
    boolean running = true;

    void worker() {
        while (running) {   // Thread B co the khong bao gio thay running = false
            doWork();
        }
    }

    void shutdown() {
        running = false;    // Thread A ghi
    }
}

Thread A (main) gọi shutdown(). Thread B (worker) đang loop trên running. Kỳ vọng: sau shutdown() return, worker thoát loop.

Thực tế: worker có thể loop mãi mãi, không thấy running = false.

Vì sao? Có 3 tầng optimization có thể tham gia:

Tầng 1 — CPU cache: CPU modern có L1 cache per core. Thread A trên core 1 ghi running = false vào L1 của core 1. Thread B trên core 2 có L1 riêng, đã cache running = true từ lần đọc đầu. Không có gì "đẩy" update từ cache core 1 sang core 2 — mỗi core tự quản cache của mình. Cache coherence protocol (MESI) sẽ eventually đẩy, nhưng khi nào không có guarantee.

Tầng 2 — JIT compiler: JIT thấy loop while (running) doWork(); — nhận thấy running không thay đổi trong loop body → optimize thành:

if (running) {
    while (true) doWork();   // Loop vo tan, khong doc lai running
}

Hợp pháp! JIT coi code như single-thread, running không đổi trong body → hoisting ra ngoài loop.

Tầng 3 — CPU register: ngay cả không JIT optimize, CPU cũng có thể giữ running trong register thay vì đọc memory mỗi iteration. Compile thành cmp eax, 1eax là copy của running load lúc đầu, không reload.

Kết quả: worker chạy forever dù A đã ghi running = false.

Solution overview

JVM cung cấp 2 công cụ fix visibility + atomicity:

  • synchronized: lock exclusive + memory barrier. Cả atomicity và visibility.
  • volatile: memory barrier + JIT hint. Chỉ visibility.

Thêm java.util.concurrent.atomic: AtomicInteger, AtomicReference — dùng CAS (compare-and-swap) hardware instruction. Atomic mà không cần lock.

3. synchronized — mutex và memory barrier

Ngữ nghĩa

synchronized làm 2 việc:

  1. Mutex: chỉ 1 thread giữ monitor của 1 object tại 1 thời điểm. Thread khác chờ (BLOCKED state).
  2. Memory barrier: khi thread release monitor (exit synchronized block), mọi ghi của nó flush về main memory. Khi thread acquire monitor, cache của nó invalidate — đọc lại từ memory.

Hai việc này combine giải quyết cả atomicity và visibility.

3 dạng cú pháp

Dạng 1 — instance method:

class Counter {
    private int value = 0;

    public synchronized void increment() {
        value++;
    }

    public synchronized int get() {
        return value;
    }
}

Lock là monitor của this. Hai thread gọi counter.increment() cùng lúc — một thread vào được, thread kia BLOCKED.

Dạng 2 — static method:

class Counter {
    private static int value = 0;

    public static synchronized void increment() {
        value++;
    }
}

Lock là Counter.class (monitor class-level). Khác monitor của instance. Instance method và static method trên cùng class không lock nhau (khác object monitor).

Dạng 3 — block:

class Counter {
    private final Object lock = new Object();
    private int value = 0;

    void increment() {
        synchronized (lock) {
            value++;
        }
    }
}

Lock trên object cụ thể bạn chọn. Linh hoạt hơn — có thể lock fine-grained (lock khác cho data khác), hoặc lock object private không expose ra ngoài.

Quy tắc: lock object private

So sánh 2 design:

// Design A: synchronized method
class Counter {
    private int value;
    public synchronized void inc() { value++; }
}

// Design B: private lock
class Counter {
    private final Object lock = new Object();
    private int value;
    public void inc() { synchronized (lock) { value++; } }
}

Design A lock là this — object Counter. Ai bên ngoài có thể:

Counter c = new Counter();
synchronized (c) {
    // Giu cung monitor voi method inc() trong Counter
    // Can tro inc() cua thread khac, hoac deadlock
}

Design B lock là lock private — không code nào ngoài Counter truy cập được. An toàn.

Đây là nguyên tắc encapsulation áp cho lock — lock là implementation detail, không nên expose. Design B là pattern chuẩn cho production code.

Reentrant

synchronized reentrant — thread đã giữ monitor có thể vào synchronized block khác với cùng monitor:

class Foo {
    public synchronized void a() {
        b();   // b() cung synchronized this - OK, cung monitor
    }

    public synchronized void b() {
        // ...
    }
}

Thread vào a() → lock this. Trong a() gọi b()b() cũng lock this. Vì cùng thread đang giữ, lock cho vào mà không block.

Không reentrant → deadlock với chính mình (đang giữ lock, lại cố lock → chờ mãi).

synchronized reentrant là lý do nó dễ dùng. ReentrantLock trong java.util.concurrent.locks cùng behavior, nhưng API rõ hơn.

4. volatile — chỉ memory barrier

Ngữ nghĩa

volatile bắt buộc 2 điều:

  1. Read/write đi thẳng main memory, không cache trong register.
  2. JIT compiler không reorder với op trước/sau nó — bảo vệ happens-before (mục 6).

Không bảo đảm atomicity cho compound op.

class Shutdown {
    private volatile boolean running = true;

    void worker() {
        while (running) {   // Dam bao thay update cua shutdown()
            doWork();
        }
    }

    void shutdown() {
        running = false;
    }
}

Thêm volatile — sau shutdown() return, worker chắc chắn thấy running = false ở iteration kế tiếp.

Use case đúng cho volatile

Chỉ khi thoả cả 2 điều kiện:

  1. Không compound op: không volatile x; x++; hay volatile x; if (x == 0) x = 1;.
  2. Ghi không phụ thuộc giá trị cũ: chỉ set giá trị mới độc lập.

Ví dụ đúng:

// Flag shutdown
volatile boolean running = true;

// Pointer tro den object immutable, hot-swap
volatile Config config;
void reload() {
    this.config = loadFromDisk();   // Khong doc config cu
}

// One-time init flag
volatile boolean initialized;

Use case SAI — volatile counter

class BadCounter {
    private volatile int value = 0;
    public void inc() { value++; }   // VAN RACE
}

value++ vẫn 3 bước. Thread A read 100, thread B read 100 (cũng là giá trị mới nhất — volatile đảm bảo visibility), cả hai +1 ghi 101. Mất update.

volatile giải quyết visibility nhưng không atomicity. Với counter → dùng AtomicInteger hoặc synchronized.

⚠️ Quy tắc đúng một câu

volatile cho "biến flag 1 chiều". synchronized hoặc AtomicXxx cho compound operation.

5. AtomicInteger và family — atomic không lock

java.util.concurrent.atomic cung cấp class wrapping primitive với op atomic dùng CAS (Compare-And-Swap) hardware instruction.

import java.util.concurrent.atomic.AtomicInteger;

AtomicInteger counter = new AtomicInteger(0);

counter.incrementAndGet();    // atomic ++ va tra gia tri moi
counter.getAndIncrement();    // atomic ++ va tra gia tri cu
counter.addAndGet(5);         // atomic += 5
counter.set(100);             // atomic set

int v = counter.get();        // atomic read

// CAS directly
boolean ok = counter.compareAndSet(10, 20);
// Neu value hien tai = 10, set = 20, tra true
// Neu khac 10, khong lam gi, tra false

CAS bên dưới

CAS là CPU instruction nguyên tử: "so sánh giá trị tại địa chỉ X với expected, nếu bằng thì ghi newValue, trả về kết quả so sánh". x86 có CMPXCHG, ARM có LDREX/STREX.

incrementAndGet loop CAS:

// Tuong duong:
int oldValue;
do {
    oldValue = get();
} while (!compareAndSet(oldValue, oldValue + 1));
return oldValue + 1;

Nếu không contention (1 thread), CAS thành công ngay → không lock OS, nhanh hơn synchronized.

Nếu có contention (nhiều thread cùng inc), retry loop — performance giảm dần. Với contention rất cao, LongAdder (Java 8) tốt hơn — chia counter thành nhiều cell, mỗi thread update cell riêng, sum cuối.

Biến thể

  • AtomicLong, AtomicBoolean, AtomicReference.
  • AtomicIntegerArray, AtomicReferenceArray.
  • LongAdder, LongAccumulator cho high-contention counter.

Khi nào dùng atomic vs synchronized:

  • Single variable, op đơn giản (inc, dec, set) → AtomicXxx nhanh hơn.
  • Multiple variable cần cập nhật cùng lúc, hoặc invariant cross-variablesynchronized. Ví dụ update cả xy để giữ x + y == 10.

6. Happens-before — luật chính thức của memory model

Java Memory Model (JMM) định nghĩa quan hệ happens-before giữa action. Nếu A happens-before B, thì B chắc chắn thấy kết quả của A.

9 rule chính (JLS §17.4.5)

  1. Program order: trong cùng thread, dòng code trước happens-before dòng sau.
  2. Monitor lock: unlock(m) happens-before lock(m) kế tiếp (bất kỳ thread nào).
  3. Volatile: write(v) happens-before read(v) kế tiếp (bất kỳ thread nào).
  4. Thread start: thread.start() happens-before mọi action trong thread mới.
  5. Thread join: mọi action trong thread X happens-before x.join() return ở thread gọi.
  6. Thread interrupt: thread.interrupt() happens-before target thread detect interrupt.
  7. Constructor finish: end constructor happens-before finalizer (ít dùng).
  8. Transitivity: A happens-before B, B happens-before C → A happens-before C.
  9. Default values: write default (0, null, false) happens-before read mọi thread.

Minh hoạ rule 2 — monitor

int data = 0;
final Object lock = new Object();

// Thread A
synchronized (lock) {
    data = 42;   // Ghi trong lock
}                // unlock happens-before lock ke tiep

// Thread B
synchronized (lock) {
    System.out.println(data);   // Chac chan thay 42
}

Rule 2 + rule 1 (program order) + rule 8 (transitivity):

  • Ghi data = 42 trước unlock(lock) → ghi happens-before unlock (program order).
  • unlock(lock) happens-before lock(lock) của thread B (rule 2).
  • lock(lock) happens-before read data của thread B (program order).
  • Transitivity: ghi data = 42 happens-before read data → thread B chắc chắn thấy 42.

Không cần volatile datasynchronized đã set up happens-before cho mọi ghi trong block.

Minh hoạ rule 5 — join

int[] result = new int[1];

Thread t = new Thread(() -> {
    result[0] = heavyCompute();   // Ghi trong thread con
});
t.start();
t.join();
System.out.println(result[0]);   // Chac chan thay

Rule 5 đảm bảo: action trong thread t happens-before t.join() return. Không cần volatile hay synchronized — thread main sau join thấy mọi update.

Tại sao hiểu happens-before quan trọng

Viết code concurrent không cần suy nghĩ happens-before từng bước — thường pattern có sẵn (synchronized block, volatile flag, AtomicXxx) đã "miễn phí" setup happens-before. Nhưng khi đọc code của người khác, hoặc debug bug concurrency hiếm gặp, happens-before là công cụ lý luận chính xác.

Câu hỏi bạn tự đặt: "thread B đọc biến X — có happens-before chain từ ghi của thread A đến đọc này không?" Nếu có → B chắc chắn thấy update. Nếu không → race condition, có thể thấy hoặc không.

7. Pitfall tổng hợp

Nhầm 1: volatile counter; counter++; nghĩ là atomic.

private volatile int count;
void inc() { count++; }   // Race!

AtomicInteger:

private final AtomicInteger count = new AtomicInteger();
void inc() { count.incrementAndGet(); }

Nhầm 2: Lock khác nhau tưởng safe.

synchronized void a() { value++; }
synchronized (new Object()) { value++; }
// Lock khac -> khong protect nhau

✅ Dùng cùng lock cho mọi access đến cùng data.

Nhầm 3: Lock quá nhỏ → miss compound op.

synchronized void set(int x) { a = x; }
synchronized int get() { return a; }
// if (get() == 0) set(1);   // Check-then-act khong atomic

✅ Lock toàn compound:

synchronized void setIfZero(int x) {
    if (a == 0) a = x;
}

Nhầm 4: synchronized trên String literal.

synchronized ("LOCK") { ... }
// String intern shared global -> class khac co the lock cung string -> deadlock

✅ Private final object:

private final Object lock = new Object();
synchronized (lock) { ... }

Nhầm 5: Double-check locking sai (cổ điển).

class Singleton {
    private static Singleton instance;   // Khong volatile
    public static Singleton get() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) instance = new Singleton();
            }
        }
        return instance;
    }
}

✅ Cần volatile:

private static volatile Singleton instance;

Hoặc tốt hơn: dùng enum singleton hoặc holder class pattern.

Nhầm 6: Thay đổi nhiều field mà chỉ mỗi field synchronized getter/setter riêng.

synchronized void setName(String n) { name = n; }
synchronized void setAge(int a) { age = a; }
// Khong bao dam name va age update cung thoi diem

✅ Lock cả operation logic:

synchronized void update(String n, int a) { name = n; age = a; }

8. 📚 Deep Dive Oracle

📚 Deep Dive Oracle

Spec / reference chính thức:

Ghi chú: JLS §17 dense nhưng chính xác. Đọc §17.4 trước (memory model overview), sau đó §17.4.5 (happens-before). Hiểu §17.4.5 là đủ viết code safe — phần còn lại là formalism cho người viết JVM. "Java Concurrency in Practice" là sách dạy concurrency tốt nhất viết bằng tiếng Anh — Brian Goetz (JDK spec lead) + Doug Lea (thiết kế j.u.c) làm co-author.

9. Tóm tắt

  • 2 vấn đề concurrency độc lập: atomicity (op không bị chia cắt) và visibility (thread thấy update thread khác).
  • value++ không atomic — 3 bytecode (read, +1, write), có thể xen kẽ.
  • Visibility bị ảnh hưởng bởi CPU cache, JIT hoisting, CPU register — không có đồng bộ, thread có thể không bao giờ thấy update của thread khác.
  • synchronized = mutex + memory barrier. Giải quyết cả atomic và visibility. Reentrant.
  • synchronized trên instance method = lock this; static method = lock Class; block = lock object chọn.
  • Quy tắc: lock object private final — tránh expose lock cho code ngoài can thiệp.
  • volatile = chỉ visibility + ngăn reorder. Không atomic cho compound op.
  • volatile counter++ vẫn race — phải AtomicInteger hoặc synchronized.
  • AtomicInteger dùng CAS hardware — atomic mà không lock OS. Nhanh hơn synchronized khi contention thấp.
  • happens-before luật: program order, monitor lock (unlock → lock), volatile (write → read), thread start/join, interrupt, transitivity.
  • Mỗi primitive concurrency (synchronized, volatile, atomic) đã setup happens-before đúng cho pattern chuẩn. Chỉ khi custom lock mới phải suy nghĩ rule từng bước.

10. Tự kiểm tra

Tự kiểm tra
Q1
Vì sao counter++ không safe dù biến là volatile?

counter++ compile ra 3 bytecode: getfield (read), iadd (+1), putfield (write). Giữa read và write, thread khác có thể chen vào đọc value cũ → cả hai ghi cùng value mới → mất 1 update.

volatile đảm bảo 2 điều:

  1. Read/write đi thẳng memory, không cache — visibility.
  2. JVM không reorder với op xung quanh — ordering.

Nhưng volatile không làm 3 bytecode thành 1 op atomic. Thread khác vẫn chen vào giữa được.

Fix 2 cách:

  • AtomicInteger.incrementAndGet() — dùng CAS hardware, atomic thực sự.
  • synchronized wrap counter++ — mutex loại trừ thread khác.
Q2
Khi nào dùng volatile thay vì synchronized?

Dùng volatile khi thoả cả 2 điều kiện:

  1. Ghi 1 chiều (1 thread ghi, N thread đọc; hoặc nhiều thread ghi cùng giá trị độc lập). Vd flag shutdown, config swap reference.
  2. Không compound op — ghi mới không phụ thuộc giá trị cũ.

Ví dụ đúng:

  • volatile boolean running; — flag ngừng loop.
  • volatile Config config; — hot-swap config object immutable.
  • volatile boolean initialized; — one-time init.

Ngược lại, cần atomic op (counter++, compare-and-set, check-then-act) hoặc update nhiều field liên quan → phải dùng synchronized hoặc AtomicXxx.

volatile nhanh hơn synchronized khi áp dụng đúng — read/write primitive, không lock OS.

Q3
Đoạn sau có race condition không, nếu cả 2 method được gọi từ nhiều thread? synchronized void a() { x++; } synchronized void b() { y++; }

Không race condition tại op đơn. Cả 2 method đều synchronized instance — lock là this. Tại 1 thời điểm chỉ 1 method chạy, x++y++ đều safe.

Nhưng cần suy xét:

  1. Nếu xy độc lập (không invariant cross-variable): design đang dùng 1 lock cho 2 field độc lập → nguồn contention không cần. Tách lock tăng throughput:
    private final Object lockX = new Object();
    private final Object lockY = new Object();
    void a() { synchronized (lockX) { x++; } }
    void b() { synchronized (lockY) { y++; } }
  2. Nếu xy có invariant (vd x + y = constant): 1 lock là đúng. Nhưng cần thêm method compound:
    synchronized void transferXToY() { x--; y++; }
    Nếu thiếu method này mà app expect invariant, có thể race với check-then-act ngoài class.

Đọc code không đủ — phải biết semantic app.

Q4
Happens-before nào đảm bảo thread B thấy update của thread A sau t.join(), mà không cần volatile hay synchronized?

Rule "Thread join" (rule 5 trong JLS §17.4.5): mọi action trong thread X happens-before x.join() trả về ở thread gọi join.

Ví dụ:

int result;
Thread t = new Thread(() -> { result = compute(); });
t.start();
t.join();
System.out.println(result);   // Chac chan thay ket qua

Chain happens-before:

  1. Ghi result = compute() happens-before end of thread t (program order trong thread).
  2. End of thread t happens-before t.join() return (rule thread join).
  3. Transitivity: ghi result happens-before read result sau join().

Đây là lý do join() + biến local là pattern đơn giản nhất cho "worker compute, main lấy kết quả" — không cần bất kỳ đồng bộ nào khác.

Cảnh báo: chỉ áp dụng cho thread bạn join. Thread khác không join không có guarantee.

Q5
Vì sao nên dùng private final Object lock thay cho synchronized (this)?

Code bên ngoài có thể synchronized (myInstance) { ... } — chiếm cùng monitor với method synchronized bên trong class. Hậu quả:

  • Deadlock: outer lock trong code khác có thể sai thứ tự với internal method, tạo deadlock chain.
  • Liveness degradation: outer giữ lock lâu (vd debugging, monitoring) → method internal BLOCKED, throughput giảm bất ngờ.
  • Phá vỡ encapsulation: lock là implementation detail của class, không nên expose cho caller thao túng.

Private final Object lock = new Object(); chỉ code trong class thấy — không ai ngoài can thiệp. Pattern chuẩn cho production code concurrent.

Lý do dùng final: đảm bảo lock không bị thay đổi reference — nếu thay lock mid-flight, thread cũ và thread mới sẽ lock 2 object khác nhau, mất đồng bộ.

Lý do dùng Object thuần: không có data ý nghĩa trong lock — chỉ dùng làm monitor. Dùng new Object() tạo instance minimal.

Q6
Khi nào AtomicInteger nhanh hơn synchronized, và khi nào chậm hơn?

Nhanh hơn khi contention thấp: 1-2 thread đồng thời update. AtomicInteger.incrementAndGet() dùng CAS hardware instruction — không syscall lock OS. Nếu CAS thành công ngay (không có thread khác chen) → gần bằng op primitive.

Chậm hơn khi contention cao: 10+ thread cùng inc 1 counter. CAS fail → retry loop. Mỗi lần fail là 1 chu kỳ CPU, không tiến triển. Với contention cực cao, throughput của AtomicInteger có thể tệ hơn synchronized.

Giải pháp cho high-contention counter: LongAdder (Java 8) — chia counter thành nhiều cell, mỗi thread update cell riêng, sum cuối. Tránh CAS retry hotspot.

Quy tắc chọn:

  • Counter thường (app hit counter, metric) → AtomicLong.
  • Counter under heavy contention (hơn 10 thread cùng inc) → LongAdder.
  • Cần update nhiều field cùng lúc hoặc invariant cross-field → synchronized.

Bài tiếp theo: ExecutorService và CompletableFuture

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