Java Internals & Concurrency/Locks — ReentrantLock, ReadWriteLock, StampedLock
6/26
Bài 6 / 26~22 phútConcurrency cơ bảnMiễn phí lượt xem

Locks — ReentrantLock, ReadWriteLock, StampedLock

java.util.concurrent.locks: tryLock timeout chống deadlock, Condition variable, ReadWriteLock cho read-heavy, StampedLock optimistic read. Khi nào Lock API vượt trội synchronized.

TL;DR: synchronized hoạt động tốt trong phần lớn trường hợp, nhưng bộc lộ giới hạn khi cần: chờ lock có timeout, interruptible wait, đọc đồng thời nhiều thread, hoặc multiple waiting set. java.util.concurrent.locks (Java 5+) cung cấp ReentrantLock, ReadWriteLock, và StampedLock — explicit lock với API linh hoạt hơn. Hiểu khi nào và vì sao dùng từng loại, cùng pattern bắt buộc try-finally để guarantee unlock, sẽ tránh được deadlock và write starvation trong production.

Bài 02 của module này đã giới thiệu synchronized và intrinsic lock — nền tảng bắt buộc trước khi đọc bài này. Bài 08 sẽ đi sâu vào CAS và Atomic class — tầng thấp hơn Lock nhưng không replace được khi cần multi-variable invariant.

1. Scenario — synchronized không đủ flexibility

Xây dựng một distributed rate limiter: nhiều thread đọc cấu hình quota liên tục, nhưng chỉ thỉnh thoảng (mỗi 5 phút) có 1 admin thread ghi lại cấu hình. Dùng synchronized:

class RateLimiterConfig {
    private volatile Map<String, Integer> quotas = new HashMap<>();
    private final Object lock = new Object();

    public synchronized int getQuota(String service) {
        return quotas.getOrDefault(service, 100);
    }

    public synchronized void reload(Map<String, Integer> newQuotas) {
        this.quotas = new HashMap<>(newQuotas);
    }
}

Vấn đề: 1000 thread đọc getQuota đồng thời — tất cả phải queue sau nhau dù đọc không modify data. Throughput thấp do read serialization không cần thiết.

Thêm bài toán thứ hai: service cần acquire 2 lock theo thứ tự A rồi B. Nếu 2 thread thử acquire ngược thứ tự (A→B và B→A) → deadlock. synchronized không cung cấp cách nào timeout sau khi chờ quá lâu — thread stuck mãi mãi.

4 giới hạn của synchronized cần giải quyết:

  1. Không timeout khi chờ lock.
  2. Không interruptible — thread bị block không thể interrupt ra.
  3. Không fairness — không đảm bảo thứ tự thread nào vào trước.
  4. Không concurrent read — mọi access đều exclusive dù chỉ đọc.

java.util.concurrent.locks sinh ra để giải quyết đúng 4 giới hạn này.

2. Interface Lock — explicit lock vs intrinsic lock

Lock (interface trong java.util.concurrent.locks, Java 5+) là abstraction cho mutual exclusion với API tường minh, khác với synchronized (gọi là intrinsic lock hay monitor lock — implicit, gắn với object monitor).

public interface Lock {
    void lock();                                      // block until acquired
    void lockInterruptibly() throws InterruptedException; // block but respect interrupt
    boolean tryLock();                                // acquire immediately or return false
    boolean tryLock(long time, TimeUnit unit)         // acquire with timeout
        throws InterruptedException;
    void unlock();                                    // release lock
    Condition newCondition();                         // create waiting set
}

So sánh nhanh:

Tính năngsynchronized (intrinsic)Lock (explicit)
AcquireTự động khi vào blocklock() thủ công
ReleaseTự động khi thoát blockunlock() thủ công — phải trong finally
TimeoutKhông cótryLock(timeout, unit)
InterruptibleKhônglockInterruptibly()
FairnessKhôngTùy chọn qua constructor
Multiple wait setKhông (1 waitset/object)Có — nhiều Condition
Concurrent readKhôngCó — qua ReadWriteLock

Nguyên tắc: dùng synchronized cho code đơn giản, chuyển sang Lock khi cần 1 trong 4 tính năng trên.

3. ReentrantLock — Lock tương đương synchronized nhưng rõ hơn

ReentrantLock là implementation cốt lõi của Lock. Về ngữ nghĩa mutual exclusion, nó tương đương synchronized: chỉ 1 thread giữ lock tại 1 thời điểm.

Reentrant nghĩa là: thread đang giữ lock có thể lock() lại cùng lock đó mà không tự deadlock. JVM track số lần re-acquire (hold count) — phải gọi unlock() đúng số lần tương ứng.

ReentrantLock lock = new ReentrantLock();

// Thread A
lock.lock();           // hold count = 1
lock.lock();           // hold count = 2 (same thread, no deadlock)
lock.unlock();         // hold count = 1
lock.unlock();         // hold count = 0, lock released

Fair vs unfair mode

ReentrantLock nhận boolean fair trong constructor:

// Unfair (default) -- cao throughput
ReentrantLock unfairLock = new ReentrantLock();
ReentrantLock unfairLock2 = new ReentrantLock(false);

// Fair -- FIFO order
ReentrantLock fairLock = new ReentrantLock(true);

Unfair (mặc định): khi lock được release, thread nào thử acquire ngay lúc đó có thể "chen hàng" trước thread đang chờ. Throughput cao hơn vì tránh context switch (thread đang running tiếp tục luôn). Nhưng thread chờ lâu có thể starve (không bao giờ acquire được nếu luôn có thread mới chen vào).

Fair: thread acquire theo thứ tự đến (FIFO). Không starvation, nhưng throughput thấp hơn 20-30% do phải đợi đúng thứ tự và context switch.

Thực tế production: dùng unfair (default) trừ khi có bằng chứng starvation thực sự xảy ra. Fairness mode thường không cần thiết vì scheduler OS cũng có cơ chế anti-starvation riêng.

4. Pattern bắt buộc — lock trong try-finally

Đây là hard rule khi dùng ReentrantLock. Không có finally, exception sẽ khiến lock không bao giờ được release → tất cả thread khác block mãi mãi.

// SAI -- exception trong criticalSection() -> lock never released
ReentrantLock lock = new ReentrantLock();
lock.lock();
criticalSection();   // may throw
lock.unlock();       // NEVER reached if exception
// DUNG -- finally guarantee unlock
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    criticalSection();
} finally {
    lock.unlock();   // always called, even on exception
}

Pattern này khác synchronizedsynchronized tự release khi thoát block (kể cả exception). ReentrantLock yêu cầu lập trình viên viết finally thủ công. Bù lại, bạn có đầy đủ control: acquire tại 1 method, release tại method khác nếu cần (ví dụ hand-off lock cho thread khác trong data structure phức tạp).

flowchart TD
    A[lock.lock] --> B[try block: critical section]
    B -->|normal| C[finally: lock.unlock]
    B -->|exception| C
    C --> D[next thread can acquire]

5. tryLock(timeout) — chống deadlock

Deadlock xảy ra khi 2 thread chờ lock mà thread kia đang giữ, tạo vòng tròn chờ. Ví dụ kinh điển:

// Thread A: giu lockA, cho lockB
synchronized (lockA) {
    synchronized (lockB) { ... }
}

// Thread B: giu lockB, cho lockA -- DEADLOCK
synchronized (lockB) {
    synchronized (lockA) { ... }
}

synchronized không có cách thoát — thread stuck vĩnh viễn. ReentrantLock.tryLock(timeout) cho phép timeout và retry:

ReentrantLock lockA = new ReentrantLock();
ReentrantLock lockB = new ReentrantLock();

// Pattern chong deadlock
boolean acquiredA = false, acquiredB = false;
try {
    acquiredA = lockA.tryLock(1, TimeUnit.SECONDS);
    acquiredB = lockB.tryLock(1, TimeUnit.SECONDS);
    if (acquiredA && acquiredB) {
        criticalSection();
    } else {
        // Could not acquire both -- retry or fail gracefully
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
} finally {
    if (acquiredB) lockB.unlock();
    if (acquiredA) lockA.unlock();
}

Nếu thread không acquire được cả 2 lock trong timeout, nó release những gì đã acquire và retry sau một delay ngẫu nhiên. Cả 2 thread đều có thể thoát khỏi tình huống deadlock — gọi là livelock phòng ngừa (detect và backoff).

Giải pháp tốt hơn khi biết thứ tự lock: Dijkstra's ordered lock acquisition — luôn acquire theo thứ tự quy ước (ví dụ: theo ID, theo hash). Nếu tất cả thread đều acquire lockA trước lockB, vòng tròn chờ không thể hình thành. Dùng khi bạn kiểm soát được code acquire lock.

6. Condition — multiple waiting set per lock

synchronized object có 1 waitset duy nhất — wait(), notify(), notifyAll(). Nếu có producer và consumer dùng cùng lock, notifyAll() đánh thức cả producer lẫn consumer — lãng phí.

Condition cho phép tạo nhiều waitset riêng biệt trên cùng 1 lock:

ReentrantLock lock = new ReentrantLock();
Condition notFull  = lock.newCondition();   // producers wait here
Condition notEmpty = lock.newCondition();   // consumers wait here

class BoundedQueue<T> {
    private final Queue<T> buf = new ArrayDeque<>();
    private final int cap;

    BoundedQueue(int cap) { this.cap = cap; }

    public void put(T item) throws InterruptedException {
        lock.lock();
        try {
            while (buf.size() == cap) notFull.await();   // wait until not full
            buf.add(item);
            notEmpty.signal();  // signal only consumers
        } finally {
            lock.unlock();
        }
    }

    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (buf.isEmpty()) notEmpty.await();  // wait until not empty
            T item = buf.poll();
            notFull.signal();   // signal only producers
            return item;
        } finally {
            lock.unlock();
        }
    }
}

Lợi ích: notEmpty.signal() chỉ đánh thức 1 consumer đang chờ — không đánh thức producer. notFull.signal() chỉ đánh thức 1 producer. Không lãng phí context switch.

Tương đương với wait/notify nhưng Condition.await() tương ứng wait(), Condition.signal() tương ứng notify(), Condition.signalAll() tương ứng notifyAll().

7. ReadWriteLock — concurrent read, exclusive write

ReadWriteLock (interface java.util.concurrent.locks.ReadWriteLock) cung cấp 2 lock:

  • Read lock — nhiều thread có thể giữ đồng thời khi không có writer.
  • Write lock — exclusive, như synchronized thông thường.

Khi nào có lợi: workload đọc nhiều, ghi ít (read-heavy). Ví dụ: config cache, reference data, lookup table. Nếu 90% request là đọc, ReadWriteLock cho phép 90% request chạy song song thay vì serialize.

ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock  = rwLock.readLock();
Lock writeLock = rwLock.writeLock();

class RateLimiterConfig {
    private Map<String, Integer> quotas = new HashMap<>();

    public int getQuota(String service) {
        readLock.lock();
        try {
            return quotas.getOrDefault(service, 100);
        } finally {
            readLock.unlock();
        }
    }

    public void reload(Map<String, Integer> newQuotas) {
        writeLock.lock();
        try {
            this.quotas = new HashMap<>(newQuotas);
        } finally {
            writeLock.unlock();
        }
    }
}

Semantics chính xác:

  • Read lock: acquire khi không có thread giữ write lock. Nhiều thread giữ read lock cùng lúc.
  • Write lock: acquire khi không có thread giữ bất kỳ lock nào (kể cả read).
Pitfall: write starvation trong fair=false mode

Nếu reader liên tục acquire read lock (read flood), writer không bao giờ có cơ hội vào — vì read lock luôn active. Tình trạng này gọi là writer starvation. Fix bằng cách dùng ReentrantReadWriteLock(true) (fair mode) — reader mới phải chờ nếu writer đang đợi. Hoặc thiết kế lại: nếu ghi thường xuyên, ReadWriteLock có thể không phù hợp, dùng StampedLock optimistic thay thế.

8. StampedLock — 3 mode, optimistic read

StampedLock (Java 8, java.util.concurrent.locks) là lock hiệu năng cao nhất trong bộ j.u.c.locks. Thiết kế bởi Doug Lea theo JEP 188. Cung cấp 3 mode:

  1. Write — exclusive, giống write lock của ReadWriteLock.
  2. Read pessimistic — shared read, giống read lock của ReadWriteLock.
  3. Read optimistickhông block writer, không giữ lock thực sự. Đọc, sau đó validate.

Optimistic read là điểm khác biệt lớn: nhiều reader không block writer, writer không block reader. Chỉ khi validate thất bại (writer đã modify) mới fallback sang pessimistic read.

StampedLock sl = new StampedLock();
double x, y;

// OPTIMISTIC READ pattern
double distanceFromOrigin() {
    long stamp = sl.tryOptimisticRead();     // not a real lock, just a stamp
    double localX = x;
    double localY = y;
    if (!sl.validate(stamp)) {               // writer modified since stamp?
        stamp = sl.readLock();               // fallback to pessimistic
        try {
            localX = x;
            localY = y;
        } finally {
            sl.unlockRead(stamp);
        }
    }
    return Math.sqrt(localX * localX + localY * localY);
}

// WRITE pattern
void moveTo(double newX, double newY) {
    long stamp = sl.writeLock();
    try {
        x = newX;
        y = newY;
    } finally {
        sl.unlockWrite(stamp);
    }
}

Quy trình optimistic read:

  1. tryOptimisticRead() — lấy stamp (version number), không block.
  2. Đọc fields vào local variable.
  3. validate(stamp) — nếu writer đã modify giữa step 1 và 3, trả false.
  4. Nếu validate fail: fallback sang readLock() (pessimistic) và đọc lại.

Khi nào tốt hơn ReadWriteLock: workload read-heavy với write rất hiếm. Khi validate thành công (99% case), không cần acquire real lock — throughput cao hơn nhiều.

9. Pitfall — StampedLock KHÔNG reentrant

StampedLock không reentrant. Đây là khác biệt quan trọng với ReentrantLocksynchronized.

StampedLock sl = new StampedLock();

void outer() {
    long stamp = sl.writeLock();
    try {
        inner();    // DEADLOCK -- inner() tries to writeLock() again
    } finally {
        sl.unlockWrite(stamp);
    }
}

void inner() {
    long stamp = sl.writeLock();   // block forever -- same thread, not reentrant
    try { ... } finally { sl.unlockWrite(stamp); }
}

StampedLock track lock state qua stamp (không phải thread identity), nó không biết thread hiện tại đã giữ lock. Thread tự deadlock với chính mình nếu cố re-acquire.

Fix: truyền stamp qua parameter, hoặc redesign để tránh recursive lock acquisition.

void outer() {
    long stamp = sl.writeLock();
    try {
        innerWithStamp(stamp);  // pass stamp, inner does not lock again
    } finally {
        sl.unlockWrite(stamp);
    }
}

10. Deadlock case study — crossed acquire

Ví dụ điển hình deadlock với 2 ReentrantLock:

ReentrantLock lockA = new ReentrantLock();
ReentrantLock lockB = new ReentrantLock();

// Thread 1
new Thread(() -> {
    lockA.lock();
    try {
        Thread.sleep(50);  // simulate work, allow Thread 2 to run
        lockB.lock();      // WAIT -- Thread 2 holds lockB
        try { doWork(); }
        finally { lockB.unlock(); }
    } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    finally { lockA.unlock(); }
}).start();

// Thread 2
new Thread(() -> {
    lockB.lock();
    try {
        Thread.sleep(50);
        lockA.lock();      // WAIT -- Thread 1 holds lockA -- DEADLOCK
        try { doWork(); }
        finally { lockA.unlock(); }
    } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    finally { lockB.unlock(); }
}).start();

Fix 1 — ordered acquisition: luôn acquire lockA trước lockB, không ngoại lệ. Không thể hình thành vòng tròn chờ.

Fix 2 — tryLock with timeout: như ví dụ ở mục 5. Không require ordering nhưng cần retry logic.

Fix 3 — single lock: nếu 2 resource thường được dùng cùng nhau, bọc trong 1 object với 1 lock duy nhất.

stateDiagram-v2
    UNLOCKED --> WRITE_LOCKED: writeLock
    UNLOCKED --> READ_LOCKED: readLock (1+)
    READ_LOCKED --> READ_LOCKED: readLock (more readers)
    READ_LOCKED --> UNLOCKED: all readUnlock
    WRITE_LOCKED --> UNLOCKED: writeUnlock
    READ_LOCKED --> WRITE_LOCKED: all readers release,\nwrite waits

11. So sánh: synchronized vs ReentrantLock vs ReadWriteLock vs StampedLock

LockTimeoutInterruptibleFair modeConcurrent readReentrantKhi nào dùng
synchronizedKhôngKhôngKhôngKhôngCode đơn giản, ít thread
ReentrantLockKhôngCần timeout, fairness, hoặc Condition
ReadWriteLockRead-heavy, write hiếm
StampedLockKhôngCó (optimistic)KhôngThroughput tối đa, write rất hiếm

12. Deep Dive

Deep Dive — spec và reference
  • java.util.concurrent.locks package Javadocdocs.oracle.com/.../locks/package-summary.html — định nghĩa chính thức Lock, ReadWriteLock, StampedLock, Condition. Đọc class-level Javadoc của ReentrantLock để hiểu fairness semantics, hold count, và reentrancy tracking.
  • JEP 188: StampedLock — không phải JEP production (JEP 188 là placeholder), tham khảo StampedLock Javadoc (Doug Lea là author) — giải thích 3 mode, validation semantics, và so sánh performance với ReadWriteLock.
  • "Java Concurrency in Practice" — Brian Goetz et al. — Chapter 13 "Explicit Locks" và Chapter 14 "Building Custom Synchronizers": giải thích khi nào Lock API vượt intrinsic lock, Condition waiting set design, và fairness analysis.
  • "Concurrent Programming in Java" — Doug Lea: nguồn gốc design j.u.c.locks. Dense nhưng là primary source cho người muốn hiểu design rationale.
  • JMH (Java Microbenchmark Harness): để đo thực tế throughput của synchronized vs ReentrantLock vs StampedLock trên workload cụ thể — kết quả phụ thuộc hardware, contention level, và workload pattern. Không assume từ lý thuyết.
  • JLS §17.1 — Synchronization: docs.oracle.com/.../jls-17.html#jls-17.1 — ngữ nghĩa chính thức của monitor lock, wait/notify; nền tảng để hiểu intrinsic lock mà Condition mở rộng.

13. Self-check

Tự kiểm tra
Q1
Vì sao phải đặt lock.unlock() trong khối finally khi dùng ReentrantLock?

synchronized tự động release khi thoát block — kể cả khi exception. ReentrantLock không có cơ chế tự động này: unlock() phải được gọi thủ công.

Nếu code trong try block throw exception và unlock() nằm sau try block (không trong finally), exception sẽ bubble up mà unlock không được gọi. Thread tiếp tục giữ lock. Mọi thread khác cố acquire lock đó sẽ block vĩnh viễn — hệ thống đơ hoàn toàn cho tài nguyên bị lock đó.

Pattern đúng là lock.lock() rồi ngay lập tức try { ... } finally { lock.unlock(); }. Lý do lock() đặt ngoài try: nếu lock() bản thân throw (không xảy ra với lock() thông thường, nhưng có thể xảy ra với lockInterruptibly()), không muốn gọi unlock() khi chưa acquire được.

Đây là một trong những nguyên nhân phổ biến nhất của deadlock trong production khi migrate từ synchronized sang ReentrantLock — lập trình viên quên finally.

Q2
Khi nào nên dùng ReentrantLock(true) (fair mode) và khi nào không cần?

Fair mode (FIFO acquisition order) phù hợp khi starvation là vấn đề thực sự: một số thread bị chặn liên tục trong khi thread mới liên tục chen vào. Thường xảy ra khi contention rất cao và một số thread ít được OS scheduler schedule hơn.

Không nên dùng mặc định fair mode vì: throughput thấp hơn 20-30% do phải đợi đúng thứ tự và context switch. Scheduler OS hiện đại (Linux CFS, Windows scheduler) đã có cơ chế anti-starvation riêng — trong thực tế starvation hiếm với unfair mode.

Quy trình quyết định: đo thực tế với production load. Nếu thấy thread latency phân phối lệch (một số thread latency rất cao trong khi median thấp), thử fair mode và benchmark lại. Nếu cải thiện latency p99 đủ để chấp nhận throughput giảm — enable fair mode.

Trường hợp điển hình cần fair mode: service processing queue với SLA fairness (mỗi request phải được xử lý trong N milliseconds, không chỉ median). Unfair mode: web server throughput-oriented.

Q3
Vì sao ReadWriteLock có thể gây writer starvation trong unfair mode, và cách nào fix?

Read lock cho phép nhiều thread đọc đồng thời. Nếu luồng reader liên tục đến (read flood), tại bất kỳ thời điểm nào cũng có ít nhất 1 reader đang giữ read lock. Write lock chỉ acquire được khi không có reader nào — điều kiện này không bao giờ thỏa mãn.

Trong unfair mode, reader mới có thể acquire read lock ngay cả khi writer đang chờ. Writer bị starve vô thời hạn trong khi reader liên tục vào.

Fix 1 — fair mode: new ReentrantReadWriteLock(true). Reader mới phải chờ nếu writer đang trong hàng chờ. Writer không bị vượt mặt bởi reader mới.

Fix 2 — thiết kế lại: nếu ghi đủ thường xuyên (vượt vài lần/giây), ReadWriteLock có thể không phù hợp. Cân nhắc StampedLock với optimistic read — không block writer hoàn toàn, chỉ invalidate optimistic stamp của reader nếu writer vừa ghi.

Fix 3 — dùng immutable snapshot: writer tạo new Map và swap reference atomic (volatile reference). Reader luôn đọc từ snapshot hiện tại không block. Phù hợp khi write không quá thường xuyên.

Q4
Giải thích quy trình optimistic read của StampedLock và vì sao nó nhanh hơn pessimistic read.

Optimistic read không acquire lock thực sự — chỉ lấy một stamp (version number) từ StampedLock internal state. Sau đó đọc fields vào biến local. Cuối cùng gọi validate(stamp) để kiểm tra xem có writer nào modify giữa lúc lấy stamp và lúc validate không.

Nếu validate thành công (không có writer modify): trả kết quả ngay. Không có lock acquire, không có memory barrier đắt tiền, không có thread queue. Throughput tối đa.

Nếu validate fail (writer đã modify): fallback sang readLock() (pessimistic), đọc lại fields với lock thực sự. Validate thất bại là exceptional case — với read-heavy workload, validate thường thành công.

Vì sao nhanh hơn pessimistic: pessimistic read phải update lock state (atomic operation) mỗi lần acquire và release — expensive với contention cao. Optimistic read chỉ đọc stamp (single load), validate cũng chỉ compare stamp — cheap. Khi validate thành công, effective cost gần bằng đọc plain field.

Trade-off: code phức tạp hơn (phải copy fields vào local, phải handle validate failure). Và StampedLock không reentrant — không dùng được nếu code có recursive lock acquisition.

Q5
Vì sao StampedLock không reentrant, và hậu quả nếu code vô tình gọi lock lần 2 từ cùng thread?

ReentrantLocksynchronized track lock ownership theo thread identity — biết thread nào đang giữ lock. Khi cùng thread gọi lock lại, nhận ra là re-entrance và tăng hold count, không block.

StampedLock track state qua stamp (version number + mode bits), không theo thread identity. Không có khái niệm "thread này đang giữ lock". Khi cùng thread gọi writeLock() lần 2, StampedLock không biết đây là re-entrance — nó thấy write lock đang bị giữ và block thread lại. Vì cùng thread, không có thread nào release lock → thread tự deadlock với chính mình. Vĩnh viễn.

Hậu quả: thread hung, task không hoàn thành, executor pool dần cạn thread, service timeout. Không exception, không log rõ ràng — chỉ thấy thread stuck trong JStack.

Phòng tránh: không dùng StampedLock trong method có thể được gọi recursive (trực tiếp hoặc qua chain). Pattern an toàn: truyền stamp qua tham số cho inner method, inner method không lock lại. Hoặc dùng ReentrantLock thay nếu cần reentrancy.

Q6
Condition.await() khác Object.wait() ở điểm nào quan trọng nhất?

Điểm khác biệt quan trọng nhất: 1 Lock có thể có nhiều Condition độc lập, nhưng 1 object monitor chỉ có 1 waitset duy nhất cho tất cả thread.

Với Object.wait/notify, nếu có 2 loại thread (producer và consumer) cùng dùng 1 lock, notifyAll() phải đánh thức tất cả — cả producer lẫn consumer, dù chỉ muốn đánh thức consumer. Overhead context switch không cần thiết; thread được đánh thức có thể phải wait lại ngay vì điều kiện chưa đúng.

Với Condition: notFull.signal() chỉ đánh thức thread đang chờ trên notFull (producer). notEmpty.signal() chỉ đánh thức consumer. Không lãng phí.

Điểm khác: Condition.await() có thể có timeout (await(long, TimeUnit)) và deadline (awaitUntil(Date)) — linh hoạt hơn wait(long millis). Cũng có awaitUninterruptibly() nếu không muốn respond interrupt.

Cú pháp khác: Condition.await() thay vì Object.wait(), Condition.signal() thay vì notify(). Phải gọi trong khi giữ lock của Condition (giống như wait() phải trong synchronized).

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