Locks và Conditions
Sau bài này, bạn sẽ:
- Hiểu được sự khác biệt giữa ReentrantLock và synchronized
- Sử dụng được tryLock() để tránh deadlock
- Nắm được Condition interface và cách dùng thay thế wait/notify
- Biết cách dùng ReadWriteLock cho read-heavy workloads
- Hiểu được StampedLock và optimistic locking
Bài trước: Concurrent Collections — Đã học về ConcurrentHashMap và BlockingQueue. Bài này sẽ giới thiệu các lock mechanisms linh hoạt hơn synchronized.
Tại sao cần Lock riêng biệt?
Từ Java 1.0, synchronized là công cụ chính để đồng bộ threads. Tuy nhiên, synchronized có nhiều hạn chế:
- Không thể "thử" acquire lock: Nếu lock đang bị giữ, thread bị block vô thời hạn — không có cách nào để "thử lock rồi làm việc khác nếu không được"
- Không có fairness policy:
synchronizedlà unfair by default và không thể thay đổi — threads có thể bị starvation - Chỉ có một wait set: Mỗi object monitor chỉ có một wait set duy nhất — không thể có nhiều conditions riêng biệt (ví dụ: "queue not full" và "queue not empty")
- Không interruptible: Thread đang chờ
synchronizedlock không thể bị interrupt — không thể cancel operation - Must release trong cùng block: Phải acquire và release lock trong cùng một block/method — không thể tách acquire ở method này, release ở method khác
Package java.util.concurrent.locks giải quyết TẤT CẢ những hạn chế này:
// synchronized: Không thể tryLock
synchronized (lock) {
// Bị block vô thời hạn nếu lock đang bị giữ
}
// Lock: Có thể tryLock
if (lock.tryLock()) {
try {
// Acquired!
} finally {
lock.unlock();
}
} else {
// Không acquire được, làm việc khác
}
- Cần tryLock() để tránh blocking/deadlock
- Cần timeout khi acquire lock
- Cần interruptible locking (có thể cancel)
- Cần fairness (FIFO ordering)
- Cần nhiều Condition variables (nhiều wait sets)
java.util.concurrent.locks Package
Package java.util.concurrent.locks cung cấp các lock mechanisms linh hoạt hơn synchronized:
import java.util.concurrent.locks.*;
Interfaces chính:
Lock: Interface cơ bảnReadWriteLock: Separate read/write locksCondition: Alternative cho wait/notify
Implementations:
ReentrantLock: Lock có thể reentrantReentrantReadWriteLock: ReadWriteLock implementationStampedLock: Optimistic reading (Java 8+)
ReentrantLock
ReentrantLock là lock có thể reentrant (thread đang giữ lock có thể acquire lại):
Reentrant có nghĩa là gì?
Reentrant = "có thể vào lại" — thread đã giữ lock có thể acquire lại lock đó mà không bị deadlock:
Lock lock = new ReentrantLock();
public void method1() {
lock.lock(); // Acquire lần 1: hold count = 1
try {
System.out.println("Method 1");
method2(); // Gọi method2, cũng cần lock
} finally {
lock.unlock(); // Release: hold count = 0
}
}
public void method2() {
lock.lock(); // Acquire lần 2 (cùng thread): hold count = 2
try {
System.out.println("Method 2");
} finally {
lock.unlock(); // Release: hold count = 1
}
}
Cơ chế:
- Lock lưu hold count (số lần lock đã được acquire)
- Mỗi lần
lock(): hold count tăng 1 - Mỗi lần
unlock(): hold count giảm 1 - Lock chỉ được release khi hold count = 0
- Số lần
unlock()phải bằng số lầnlock()— nếu không sẽ không bao giờ release lock (deadlock)! - Cả
synchronizedvàReentrantLockđều reentrant — synchronized cũng cho phép thread acquire lại monitor của object
Ví dụ synchronized reentrant:
public synchronized void method1() {
System.out.println("Method 1");
method2(); // Cùng thread acquire monitor lại
}
public synchronized void method2() {
System.out.println("Method 2"); // OK, không deadlock
}
Cấu trúc cơ bản
import java.util.concurrent.locks.*;
public class ReentrantLockDemo {
private final Lock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock(); // Acquire lock
try {
count++;
System.out.println(Thread.currentThread().getName() +
" incremented to " + count);
} finally {
lock.unlock(); // LUÔN unlock trong finally
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
- LUÔN gọi unlock() trong finally block để đảm bảo unlock ngay cả khi có exception
- Số lần lock() phải bằng unlock(): Không unlock → deadlock!
tryLock() - Non-blocking Lock
import java.util.concurrent.locks.*;
import java.util.concurrent.TimeUnit;
public class TryLockDemo {
private final Lock lock = new ReentrantLock();
public void method1() {
// Thử acquire lock, không chờ
if (lock.tryLock()) {
try {
System.out.println("Lock acquired by " +
Thread.currentThread().getName());
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} else {
System.out.println("Could not acquire lock, doing other work");
}
}
public void method2() throws InterruptedException {
// Thử acquire lock với timeout
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println("Lock acquired with timeout");
} finally {
lock.unlock();
}
} else {
System.out.println("Timeout! Could not acquire lock");
}
}
public static void main(String[] args) {
TryLockDemo demo = new TryLockDemo();
Thread t1 = new Thread(() -> demo.method1(), "Thread-1");
Thread t2 = new Thread(() -> demo.method1(), "Thread-2");
t1.start();
try { Thread.sleep(100); } catch (InterruptedException e) {}
t2.start();
}
}
Output:
Lock acquired by Thread-1
Could not acquire lock, doing other work
lockInterruptibly() - Interruptible Lock
public void interruptibleMethod() throws InterruptedException {
lock.lockInterruptibly(); // Có thể bị interrupt trong khi chờ lock
try {
// Critical section
while (!Thread.interrupted()) {
// Do work
}
} finally {
lock.unlock();
}
}
Fair Lock
// Fair lock: Thread chờ lâu nhất được ưu tiên (FIFO)
Lock fairLock = new ReentrantLock(true);
// Unfair lock (default): Không đảm bảo thứ tự, nhưng nhanh hơn
Lock unfairLock = new ReentrantLock(false);
- Fair lock: Tránh starvation, nhưng throughput thấp hơn
- Unfair lock: Throughput cao hơn, nhưng có thể xảy ra starvation
ReentrantLock vs synchronized
| Feature | ReentrantLock | synchronized |
|---|---|---|
| API | Explicit lock/unlock | Implicit (enter/exit block) |
| try-lock | Có (tryLock()) | Không |
| Timeout | Có (tryLock(time)) | Không |
| Interruptible | Có (lockInterruptibly()) | Không |
| Fairness | Có (optional) | Không |
| Condition variables | Nhiều Conditions | 1 monitor (wait/notify) |
| Performance | Tương đương | Tương đương |
| Complexity | Phức tạp hơn | Đơn giản hơn |
| Forget unlock | Có thể quên → deadlock | Tự động unlock |
Khi nào dùng ReentrantLock?
Dùng ReentrantLock khi:
- Cần
tryLock()để tránh deadlock - Cần timeout khi acquire lock
- Cần interruptible locking
- Cần fairness
- Cần nhiều Condition variables
Dùng synchronized khi:
- Đơn giản, không cần features nâng cao
- Không muốn lo lắng quên unlock
- Code dễ đọc hơn
Condition Interface
Condition là alternative cho wait(), notify(), notifyAll():
public interface Condition {
void await() throws InterruptedException; // Như wait()
boolean await(long time, TimeUnit unit); // wait() với timeout
void signal(); // Như notify()
void signalAll(); // Như notifyAll()
}
Ví dụ: Producer-Consumer với Condition
import java.util.concurrent.locks.*;
import java.util.LinkedList;
import java.util.Queue;
public class ProducerConsumerCondition {
private final Queue<Integer> queue = new LinkedList<>();
private final int CAPACITY = 5;
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // Queue chưa đầy
private final Condition notEmpty = lock.newCondition(); // Queue chưa rỗng
public void produce(int value) throws InterruptedException {
lock.lock();
try {
// Chờ cho đến khi queue chưa đầy
while (queue.size() == CAPACITY) {
System.out.println("Queue full. Producer waiting...");
notFull.await();
}
queue.add(value);
System.out.println("Produced: " + value + " | Queue size: " + queue.size());
// Thông báo cho consumers
notEmpty.signal();
} finally {
lock.unlock();
}
}
public int consume() throws InterruptedException {
lock.lock();
try {
// Chờ cho đến khi queue chưa rỗng
while (queue.isEmpty()) {
System.out.println("Queue empty. Consumer waiting...");
notEmpty.await();
}
int value = queue.poll();
System.out.println("Consumed: " + value + " | Queue size: " + queue.size());
// Thông báo cho producers
notFull.signal();
return value;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ProducerConsumerCondition pc = new ProducerConsumerCondition();
// Producer thread
Thread producer = new Thread(() -> {
try {
for (int i = 1; i <= 10; i++) {
pc.produce(i);
Thread.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// Consumer thread
Thread consumer = new Thread(() -> {
try {
for (int i = 1; i <= 10; i++) {
pc.consume();
Thread.sleep(1000); // Consumer chậm hơn
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producer.start();
consumer.start();
}
}
Spurious Wakeup và Tại sao phải dùng while
Spurious wakeup = "đánh thức giả" — await() có thể return mà KHÔNG có signal() nào được gọi!
// SAI - Dùng if
lock.lock();
try {
if (queue.isEmpty()) { // ❌ KHÔNG dùng if
notEmpty.await();
}
// Có thể spurious wakeup → queue vẫn empty!
queue.poll(); // NullPointerException!
} finally {
lock.unlock();
}
// ĐÚNG - Dùng while
lock.lock();
try {
while (queue.isEmpty()) { // ✅ Luôn dùng while
notEmpty.await();
}
// Guarantee: queue không empty
queue.poll(); // Safe
} finally {
lock.unlock();
}
Tại sao xảy ra spurious wakeup?
- Hệ điều hành có thể đánh thức thread bất cứ lúc nào (OS-level optimization)
- JVM không đảm bảo
await()chỉ return khi cósignal() - Đây là behavior hợp lệ theo Java specification
Quy tắc:
// LUÔN LUÔN dùng while loop
while (!condition) {
conditionVariable.await();
}
Spurious wakeup xảy ra với cả Object.wait() và Condition.await() — luôn dùng while loop cho cả hai!
// Object.wait() - cũng cần while
synchronized (lock) {
while (!condition) {
lock.wait(); // Spurious wakeup có thể xảy ra
}
}
Ưu điểm của Condition
-
Nhiều Conditions: Một Lock có thể có nhiều Conditions riêng biệt
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
Condition customCondition = lock.newCondition(); -
Rõ ràng hơn wait/notify: Mỗi condition có ý nghĩa cụ thể
notEmpty.signal(); // Rõ ràng: signal "queue not empty"
lock.notify(); // Không rõ: notify cái gì? -
Interruptible:
await()có thể bị interrupt
ReadWriteLock
ReadWriteLock cho phép:
- Nhiều readers đồng thời (shared lock)
- Chỉ 1 writer (exclusive lock)
- Writer block tất cả readers và writers
Read Lock (Shared)
Thread 1: ████████ (reading)
Thread 2: ████████ (reading) ← Nhiều readers cùng lúc
Thread 3: ████████ (reading)
Write Lock (Exclusive)
Thread 4: ░░░░████████░░░░ (writing) ← Chỉ 1 writer
↑ ↑
Block readers Release
Khi nào dùng ReadWriteLock?
Dùng khi:
- Đọc nhiều hơn ghi (read-heavy workload)
- Nhiều threads đọc cùng data
- Write operations ít xảy ra
Không dùng khi:
- Write nhiều (overhead không đáng)
- Cần simplicity
ReentrantReadWriteLock
import java.util.concurrent.locks.*;
public class ReentrantReadWriteLockDemo {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
private int value = 0;
// Multiple readers can execute this concurrently
public int read() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() +
" reading: " + value);
Thread.sleep(1000); // Giả lập read operation
return value;
} catch (InterruptedException e) {
e.printStackTrace();
return -1;
} finally {
readLock.unlock();
}
}
// Only one writer can execute this
public void write(int newValue) {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() +
" writing: " + newValue);
value = newValue;
Thread.sleep(1000); // Giả lập write operation
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
public static void main(String[] args) {
ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
// 5 reader threads
for (int i = 1; i <= 5; i++) {
new Thread(() -> demo.read(), "Reader-" + i).start();
}
// 1 writer thread
new Thread(() -> demo.write(100), "Writer-1").start();
// More readers
try { Thread.sleep(500); } catch (InterruptedException e) {}
for (int i = 6; i <= 8; i++) {
new Thread(() -> demo.read(), "Reader-" + i).start();
}
}
}
Output:
Reader-1 reading: 0
Reader-2 reading: 0 ← Readers chạy song song
Reader-3 reading: 0
Reader-4 reading: 0
Reader-5 reading: 0
Writer-1 writing: 100 ← Writer chờ readers xong
Reader-6 reading: 100 ← Readers tiếp tục sau writer
Reader-7 reading: 100
Reader-8 reading: 100
Ví dụ thực tế: Read-Heavy Cache
import java.util.concurrent.locks.*;
import java.util.*;
public class ReadHeavyCache {
private final Map<String, String> cache = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
// Read operation (nhiều threads có thể đọc cùng lúc)
public String get(String key) {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() +
" reading key: " + key);
simulateWork(100); // Giả lập read từ cache
return cache.get(key);
} finally {
readLock.unlock();
}
}
// Write operation (chỉ 1 thread)
public void put(String key, String value) {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() +
" writing key: " + key);
simulateWork(500); // Giả lập expensive operation
cache.put(key, value);
} finally {
writeLock.unlock();
}
}
// Upgrade lock (KHÔNG ĐƯỢC HỖ TRỢ TRỰC TIẾP)
public void updateIfExists(String key, String newValue) {
readLock.lock();
try {
if (cache.containsKey(key)) {
// PHẢI release read lock trước khi acquire write lock
readLock.unlock();
writeLock.lock();
try {
// Double-check
if (cache.containsKey(key)) {
cache.put(key, newValue);
}
} finally {
// Downgrade: acquire read lock trước khi release write lock
readLock.lock();
writeLock.unlock();
}
}
} finally {
readLock.unlock();
}
}
private void simulateWork(int ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) {}
}
public static void main(String[] args) throws InterruptedException {
ReadHeavyCache cache = new ReadHeavyCache();
// Initialize cache
cache.put("user:1", "Alice");
cache.put("user:2", "Bob");
// 10 reader threads
for (int i = 1; i <= 10; i++) {
final int id = i % 2 + 1;
new Thread(() -> {
for (int j = 0; j < 3; j++) {
String value = cache.get("user:" + id);
System.out.println(" Got: " + value);
}
}, "Reader-" + i).start();
}
// 2 writer threads
Thread.sleep(1000);
new Thread(() -> cache.put("user:3", "Charlie"), "Writer-1").start();
new Thread(() -> cache.put("user:4", "David"), "Writer-2").start();
}
}
ReadWriteLock KHÔNG hỗ trợ lock upgrade trực tiếp (read → write). Phải:
- Release read lock
- Acquire write lock
- Double-check condition
- (Optional) Downgrade: acquire read lock trước khi release write lock
StampedLock (Java 8+)
StampedLock là lock cao cấp hơn với optimistic reading:
StampedLock KHÔNG reentrant — nếu cùng thread cố gắng acquire lại lock, sẽ bị DEADLOCK!
StampedLock lock = new StampedLock();
public void method1() {
long stamp = lock.writeLock(); // Acquire
try {
method2(); // Gọi method2
} finally {
lock.unlockWrite(stamp);
}
}
public void method2() {
long stamp = lock.writeLock(); // DEADLOCK! Same thread
try {
// Never executes
} finally {
lock.unlockWrite(stamp);
}
}
Lý do: StampedLock được thiết kế cho maximum performance ở read-heavy workloads, không hỗ trợ reentrancy để giảm overhead.
Khác biệt:
ReentrantLock: Reentrant ✅ReentrantReadWriteLock: Reentrant ✅StampedLock: KHÔNG reentrant ❌
3 modes
- Write Lock: Exclusive lock (như write lock của ReadWriteLock)
- Read Lock: Shared lock (như read lock của ReadWriteLock)
- Optimistic Read: Không lock, chỉ kiểm tra version
import java.util.concurrent.locks.*;
public class StampedLockDemo {
private final StampedLock lock = new StampedLock();
private double x = 0, y = 0;
// Write
public void move(double deltaX, double deltaY) {
long stamp = lock.writeLock(); // Exclusive lock
try {
x += deltaX;
y += deltaY;
} finally {
lock.unlockWrite(stamp);
}
}
// Optimistic Read
public double distanceFromOrigin() {
long stamp = lock.tryOptimisticRead(); // Không lock!
double currentX = x;
double currentY = y;
if (!lock.validate(stamp)) { // Kiểm tra có bị modify không
// Nếu có write xảy ra, upgrade lên read lock
stamp = lock.readLock();
try {
currentX = x;
currentY = y;
} finally {
lock.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
// Pessimistic Read
public double getX() {
long stamp = lock.readLock();
try {
return x;
} finally {
lock.unlockRead(stamp);
}
}
// Convert read lock to write lock
public void moveIfAtOrigin(double deltaX, double deltaY) {
long stamp = lock.readLock();
try {
while (x == 0.0 && y == 0.0) {
// Thử convert read lock → write lock
long writeStamp = lock.tryConvertToWriteLock(stamp);
if (writeStamp != 0L) { // Conversion thành công
stamp = writeStamp;
x = deltaX;
y = deltaY;
break;
} else { // Conversion thất bại
lock.unlockRead(stamp);
stamp = lock.writeLock(); // Acquire write lock
}
}
} finally {
lock.unlock(stamp);
}
}
public static void main(String[] args) {
StampedLockDemo demo = new StampedLockDemo();
// Writers
for (int i = 0; i < 5; i++) {
final int id = i;
new Thread(() -> {
demo.move(id, id * 2);
System.out.println("Moved to (" + demo.getX() + ", " + demo.y + ")");
}, "Writer-" + i).start();
}
// Readers (optimistic)
for (int i = 0; i < 10; i++) {
new Thread(() -> {
double distance = demo.distanceFromOrigin();
System.out.println("Distance: " + distance);
}, "Reader-" + i).start();
}
}
}
Optimistic Read Flow
1. stamp = tryOptimisticRead() // Không lock, lấy version
2. Đọc dữ liệu // Có thể bị concurrent write
3. validate(stamp) // Kiểm tra version có thay đổi không?
- Nếu valid: Dùng dữ liệu đã đọc
- Nếu invalid: Upgrade lên read lock và đọc lại
Dùng khi:
- Read RẤT NHIỀU hơn write (95%+ reads)
- Muốn tối ưu performance tối đa
- Có thể chấp nhận complexity cao
Không dùng khi:
- Cần reentrancy (StampedLock không reentrant)
- Code cần đơn giản, dễ maintain
- Write operations nhiều
StampedLock vs ReentrantReadWriteLock
| Feature | StampedLock | ReentrantReadWriteLock |
|---|---|---|
| Optimistic read | Có | Không |
| Performance (read-heavy) | Tốt hơn | Tốt |
| Reentrancy | Không | Có |
| Complexity | Cao | Medium |
| Lock conversion | Có | Không |
Best Practices
-
Luôn unlock trong finally
lock.lock();
try {
// Critical section
} finally {
lock.unlock(); // Đảm bảo unlock
} -
Dùng tryLock để tránh deadlock
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
// Work
} finally {
lock.unlock();
}
} else {
// Handle failure
} -
await() trong while loop
while (condition) {
condition.await(); // Không dùng if
} -
Choose appropriate lock:
- Simple:
synchronized - Advanced features:
ReentrantLock - Read-heavy:
ReentrantReadWriteLock - Read-very-heavy:
StampedLock
- Simple:
-
Fair locks cho critical fairness
Lock fairLock = new ReentrantLock(true); -
Avoid lock upgrade: Release read, acquire write
Bài tập
Bài 1: Thread-Safe Counter với tryLock
Implement counter:
- Dùng
tryLock()để tránh blocking - Nếu không acquire được lock, retry hoặc skip
- Track số lần failed acquisitions
Bài 2: Read-Write Cache
Implement cache với ReadWriteLock:
get(key): Read lockput(key, value): Write lockcomputeIfAbsent(key, function): Lock upgrade pattern- Benchmark với 90% reads, 10% writes
Bài 3: Bounded Buffer với Conditions
Implement bounded buffer:
put(item): Chờ nếu full (Condition notFull)take(): Chờ nếu empty (Condition notEmpty)size(): Return current size- Test với multiple producers/consumers
Bài 4: Optimistic Locking
Implement counter với StampedLock optimistic read:
increment(): Write lockget(): Optimistic read- Measure performance vs ReentrantLock
Các điểm quan trọng cho thi OCP:
-
Lock phải unlock trong finally block
lock.lock();
try {
// Critical section
} finally {
lock.unlock(); // BẮT BUỘC trong finally
}Pattern này là BẮT BUỘC — nếu không có finally, có thể leak lock khi có exception!
-
ReentrantLock is reentrant, StampedLock is NOT
ReentrantLock: Thread có thể acquire lại ✅ReentrantReadWriteLock: Thread có thể acquire lại ✅StampedLock: DEADLOCK nếu acquire lại ❌
-
ReadWriteLock: nhiều readers HOẶC single writer
// Có thể:
Reader 1 + Reader 2 + Reader 3 ✅
Writer 1 (alone) ✅
// KHÔNG thể:
Reader 1 + Writer 1 ❌
Writer 1 + Writer 2 ❌ -
tryLock() vs tryLock(timeout)
tryLock() // Return immediately
tryLock(1, SECONDS) // Wait up to 1 second -
Condition.await() releases lock
await()release lock (giốngObject.wait())- Thread khác có thể acquire lock
signal()wake thread nhưng không release lock — thread được wake vẫn phải chờ lock
-
Fair lock: new ReentrantLock(true)
Lock fairLock = new ReentrantLock(true); // FIFO ordering
Lock unfairLock = new ReentrantLock(false); // Default -
lock() vs lockInterruptibly()
lock.lock(); // KHÔNG thể interrupt
lock.lockInterruptibly(); // Có thể interruptNếu thread đang chờ
lockInterruptibly(), có thể bịinterrupt()→ throwInterruptedException.
Tóm tắt
- ReentrantLock: Lock linh hoạt với tryLock, timeout, interruptible
- Condition: Alternative cho wait/notify, nhiều conditions per lock
- ReadWriteLock: Nhiều readers đồng thời, 1 writer
- StampedLock: Optimistic reading cho read-very-heavy workloads
- Best practice: Luôn unlock trong finally, chọn lock phù hợp
Bài tiếp theo: Multithreading Best Practices - Tổng hợp best practices và common pitfalls!
Đọc thêm
- Oracle: Lock Objects
- Java Concurrency in Practice - Chapter 13: Explicit Locks
- Effective Java (3rd Edition) - Item 82: Document thread safety
- Lock API Javadoc
- Condition API Javadoc
- Bài trước: Synchronization Basics — So sánh synchronized vs Lock