Chuyển tới nội dung chính

Synchronization

Mục tiêu bài học

Sau bài này, bạn sẽ:

  • Hiểu được vấn đề Race Condition và cách giải quyết với synchronized
  • Sử dụng được synchronized keyword (method và block)
  • Nắm được khái niệm Intrinsic Lock (Monitor Lock)
  • Hiểu được sự khác biệt giữa volatile và synchronized
  • Nhận biết và phòng tránh Deadlock
  • Sử dụng được wait(), notify(), notifyAll() để điều phối threads

Bài trước: Tạo và quản lý Thread — Đã học cách tạo threads với Thread, Runnable, Callable. Bài này sẽ giải quyết vấn đề Race Condition thông qua synchronization.

Vấn đề: Race Condition và Data Corruption

Khi nhiều threads cùng truy cập và thay đổi shared data, có thể xảy ra race condition:

public class BankAccount {
private int balance = 1000;

public void withdraw(int amount) {
if (balance >= amount) {
// Giả sử context switch xảy ra ở đây!
System.out.println(Thread.currentThread().getName() +
" withdrawing " + amount);
balance -= amount;
System.out.println("Remaining: " + balance);
} else {
System.out.println("Insufficient balance");
}
}

public int getBalance() {
return balance;
}
}

public class RaceConditionProblem {
public static void main(String[] args) throws InterruptedException {
BankAccount account = new BankAccount();

Thread t1 = new Thread(() -> account.withdraw(800), "Customer-1");
Thread t2 = new Thread(() -> account.withdraw(800), "Customer-2");

t1.start();
t2.start();

t1.join();
t2.join();

System.out.println("Final balance: " + account.getBalance());
}
}

Kết quả (có thể):

Customer-1 withdrawing 800
Customer-2 withdrawing 800
Remaining: 200
Remaining: -600 ← Số dư âm! Không hợp lệ!
Final balance: -600
Vấn đề

Cả 2 threads đều kiểm tra balance >= 800 (đúng), nhưng rút tiền gần như đồng thời → số dư âm!

Synchronized Keyword

synchronized đảm bảo chỉ một thread được thực thi code block tại một thời điểm.

1. Synchronized Method

public class BankAccount {
private int balance = 1000;

// Synchronized method: chỉ 1 thread được vào tại 1 thời điểm
public synchronized void withdraw(int amount) {
if (balance >= amount) {
System.out.println(Thread.currentThread().getName() +
" withdrawing " + amount);
balance -= amount;
System.out.println("Remaining: " + balance);
} else {
System.out.println(Thread.currentThread().getName() +
": Insufficient balance");
}
}

public synchronized void deposit(int amount) {
balance += amount;
System.out.println(Thread.currentThread().getName() +
" deposited " + amount + ", balance: " + balance);
}

public synchronized int getBalance() {
return balance;
}
}

Kết quả (thread-safe):

Customer-1 withdrawing 800
Remaining: 200
Customer-2: Insufficient balance ← Thread 2 chờ thread 1 xong mới vào
Final balance: 200

2. Synchronized Block

Synchronized block cho phép kiểm soát phạm vi lock nhỏ hơn:

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

public void increment() {
// Code không cần đồng bộ
System.out.println("Before sync");

synchronized (lock) {
// Chỉ phần này cần đồng bộ
count++;
}

// Code không cần đồng bộ
System.out.println("After sync");
}

public int getCount() {
synchronized (lock) {
return count;
}
}
}
Synchronized block vs method

Block: Linh hoạt hơn, lock phạm vi nhỏ hơn → hiệu suất tốt hơn Method: Ngắn gọn hơn, lock toàn bộ method

3. Synchronized trên this

public void method() {
synchronized (this) {
// Tương đương synchronized method
}
}

4. Synchronized trên Class Object (Static)

public class Counter {
private static int count = 0;

// Synchronized static method: lock trên Counter.class
public static synchronized void increment() {
count++;
}

// Tương đương với:
public static void incrementAlt() {
synchronized (Counter.class) {
count++;
}
}
}

Intrinsic Lock (Monitor Lock)

Mỗi object trong Java có một intrinsic lock (monitor):

synchronized (obj) {
// Thread giữ lock của obj
// Threads khác phải chờ
}
// Lock được release khi ra khỏi block

Quy tắc:

  • Thread phải acquire lock trước khi vào synchronized block
  • Chỉ một thread có thể giữ lock tại một thời điểm
  • Lock được tự động release khi thoát block (kể cả khi có exception)
Thread 1: ┌─────────┐ [acquire lock] → [execute] → [release lock]
Thread 2: └─ [wait] ─────────────────→ [acquire lock] → [execute]

Java Memory Model (JMM) và Happens-Before

Để hiểu tại sao cần đồng bộ hóa (synchronization), phải hiểu Java Memory Model (JMM) — bộ quy tắc định nghĩa cách threads tương tác thông qua memory.

Vấn đề ở mức phần cứng

┌──────────┐     ┌──────────┐
│ CPU 1 │ │ CPU 2 │
│ Thread1 │ │ Thread2 │
└────┬─────┘ └────┬─────┘
│ │
┌──▼───┐ ┌──▼───┐
│ L1 $ │ │ L1 $ │ ← CPU cache: nhanh nhưng local
└──┬───┘ └──┬───┘
│ │
┌──▼───────────────▼───┐
│ L2 Cache │
└──────────┬───────────┘

┌──────────▼───────────┐
│ Main Memory │ ← Chậm nhưng shared
└──────────────────────┘

Vấn đề:

  1. CPU Cache: Mỗi CPU có cache riêng → Thread 1 ghi vào cache, Thread 2 đọc từ cache cũ → stale data
  2. Store Buffer: CPU ghi vào buffer trước khi flush về memory → độ trễ visibility
  3. Instruction Reordering: Compiler/CPU có thể sắp xếp lại lệnh để tối ưu hóa → thứ tự execution khác source code
// Code gốc
int a = 0;
boolean ready = false;

// Thread 1
a = 42;
ready = true;

// Thread 2
if (ready) {
System.out.println(a); // Có thể in 0 thay vì 42!
}

Tại sao? CPU có thể reorder: ready = true thực thi trước a = 42 → Thread 2 thấy ready == true nhưng a vẫn là 0.

Quy tắc Happens-Before (JLS 17.4.5)

JMM định nghĩa happens-before relationship — đảm bảo thứ tự visibility giữa operations:

Nếu action A happens-before action B, thì tất cả thay đổi của A đều visible cho B.

6 quy tắc quan trọng:

1. Program Order Rule

Trong cùng một thread, mỗi action happens-before các actions sau nó (theo source code order).

int x = 1;        // Action 1
int y = x + 1; // Action 2 thấy được x = 1

2. Monitor Lock Rule

Unlock một monitor happens-before mọi lock sau đó trên cùng monitor.

synchronized (lock) {
x = 42; // Action trong thread 1
}
// Unlock happens-before lock tiếp theo

synchronized (lock) {
// Action trong thread 2 thấy được x = 42
System.out.println(x);
}

3. Volatile Variable Rule

Ghi vào volatile variable happens-before mọi lần đọc sau đó.

volatile boolean ready = false;

// Thread 1
data = 42;
ready = true; // Volatile write happens-before...

// Thread 2
if (ready) { // ...volatile read
System.out.println(data); // Thấy được data = 42
}

4. Thread Start Rule

thread.start() happens-before mọi action trong thread đó.

int x = 0;
Thread t = new Thread(() -> {
System.out.println(x); // Thấy được x = 0
});
x = 42;
t.start(); // start() happens-before println

5. Thread Termination Rule

Mọi action trong thread happens-before thread.join() return thành công.

Thread t = new Thread(() -> {
x = 42; // Action trong thread
});
t.start();
t.join(); // Chờ thread kết thúc
System.out.println(x); // Thấy được x = 42

6. Transitivity

Nếu A happens-before B, và B happens-before C, thì A happens-before C.

// Thread 1
x = 1; // A
synchronized (lock) { // B (lock)
y = 2;
} // Release lock

// Thread 2
synchronized (lock) { // C (lock, happens-after B)
System.out.println(y); // Thấy y = 2
System.out.println(x); // Thấy x = 1 (do transitivity)
}

Analogy: JMM như "Hợp đồng"

Hãy tưởng tượng JMM là hợp đồng giữa bạn (developer) và JVM:

  • Bạn cam kết: Sử dụng synchronized/volatile đúng cách
  • JVM cam kết: Đảm bảo happens-before → visibility

Nếu bạn không dùng synchronization, JVM không đảm bảo thứ tự visibility → race conditions, stale data.

Tại sao cần JMM?

JMM cho phép JVM tối ưu hóa (reordering, caching) trong khi vẫn đảm bảo tính đúng đắn của concurrent programs.

Trade-off (sự đánh đổi): Performance vs Correctness — JMM cân bằng giữa 2 yếu tố này.

Volatile Keyword

volatile đảm bảo visibility của biến giữa các threads:

Vấn đề không dùng volatile

public class VolatileProblem {
private boolean running = true; // Thread 2 có thể cache giá trị này

public void run() {
new Thread(() -> {
while (running) {
// Do work
}
System.out.println("Thread stopped");
}).start();
}

public void stop() {
running = false; // Thread 2 có thể không thấy thay đổi!
}
}

Giải pháp với volatile

public class VolatileSolution {
private volatile boolean running = true; // Đảm bảo visibility

public void run() {
new Thread(() -> {
while (running) {
// Do work
}
System.out.println("Thread stopped");
}).start();
}

public void stop() {
running = false; // Thread 2 sẽ thấy ngay lập tức
}
}

Cách volatile hoạt động: Memory Fence (Barrier)

Khi sử dụng volatile, JVM chèn memory barriers (rào cản bộ nhớ):

Ghi volatile:
[normal writes]
──────────────────
StoreStore barrier ← Đảm bảo writes trước flush xong
──────────────────
[volatile write]
──────────────────
StoreLoad barrier ← Đảm bảo volatile write visible trước reads sau
──────────────────

Đọc volatile:
──────────────────
LoadLoad barrier ← Đảm bảo volatile read xong trước loads sau
──────────────────
[volatile read]
──────────────────
LoadStore barrier ← Đảm bảo volatile read xong trước stores sau
──────────────────
[normal reads/writes]

Kết quả:

  • Ghi vào volatile variable → flush tất cả changes về main memory
  • Đọc từ volatile variable → refresh từ main memory (không dùng cache)

volatile tạo Happens-Before Edge

class SharedData {
private int data = 0;
private volatile boolean ready = false; // volatile flag

// Thread 1: Producer
public void produce() {
data = 42; // (1) Normal write
ready = true; // (2) Volatile write → happens-before edge
}

// Thread 2: Consumer
public void consume() {
if (ready) { // (3) Volatile read → thấy ready = true
// Do transitivity, thread 2 cũng thấy data = 42
System.out.println(data); // In ra 42
}
}
}

Giải thích:

  • Ghi ready = true (volatile) happens-before đọc ready trong thread 2
  • Do transitivity, data = 42 cũng happens-before đọc data
  • → Thread 2 thấy cả ready = truedata = 42

volatile KHÔNG đảm bảo Atomicity

public class VolatileCounterBroken {
private volatile int count = 0; // volatile KHÔNG giúp gì ở đây!

public void increment() {
count++; // Compound operation: read → modify → write
}

public static void main(String[] args) throws InterruptedException {
VolatileCounterBroken counter = new VolatileCounterBroken();

Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
});
threads[i].start();
}

for (Thread t : threads) t.join();

System.out.println("Expected: 10000");
System.out.println("Actual: " + counter.count); // Có thể < 10000!
}
}

Tại sao? count++ thực chất là:

int temp = count;  // Read
temp = temp + 1; // Modify
count = temp; // Write

Giữa 3 bước này, thread khác có thể interleaved (xen kẽ) → race condition.

Sửa: Dùng AtomicInteger hoặc synchronized:

private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // Atomic operation
}
volatile vs synchronized
Featurevolatilesynchronized
Visibility (tính khả kiến)
Atomicity (tính nguyên tử)Chỉ với single read/writeCó (toàn bộ block)
Ordering (thứ tự)Tạo happens-before edgeTạo happens-before edge
PerformanceNhanh hơn (no locking overhead)Chậm hơn (locking overhead, chi phí phụ)
Use caseFlags, status variablesComplex operations

Khi nào dùng volatile:

  • Simple flags: volatile boolean done
  • Single read/write operations
  • Không cần atomicity cho nhiều operations
  • Example: Double-checked locking pattern

Khi nào dùng synchronized:

  • Complex operations: count++, balance -= amount
  • Nhiều bước phải atomic
  • Cần mutual exclusion (loại trừ lẫn nhau)

Deadlock

Deadlock xảy ra khi 2+ threads chờ nhau mãi mãi:

public class DeadlockDemo {
private final Object lock1 = new Object();
private final Object lock2 = new Object();

public void method1() {
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() +
": Holding lock1...");

try { Thread.sleep(100); } catch (InterruptedException e) {}

System.out.println(Thread.currentThread().getName() +
": Waiting for lock2...");
synchronized (lock2) {
System.out.println("Thread 1: Acquired lock2");
}
}
}

public void method2() {
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() +
": Holding lock2...");

try { Thread.sleep(100); } catch (InterruptedException e) {}

System.out.println(Thread.currentThread().getName() +
": Waiting for lock1...");
synchronized (lock1) {
System.out.println("Thread 2: Acquired lock1");
}
}
}

public static void main(String[] args) {
DeadlockDemo demo = new DeadlockDemo();

Thread t1 = new Thread(() -> demo.method1(), "Thread-1");
Thread t2 = new Thread(() -> demo.method2(), "Thread-2");

t1.start();
t2.start();
}
}

Output (Deadlock):

Thread-1: Holding lock1...
Thread-2: Holding lock2...
Thread-1: Waiting for lock2...
Thread-2: Waiting for lock1...
[Chương trình treo mãi mãi]

4 điều kiện Deadlock (Coffman Conditions)

Deadlock xảy ra khi tất cả 4 điều kiện này đúng:

  1. Mutual Exclusion: Tài nguyên chỉ 1 thread sử dụng tại 1 thời điểm
  2. Hold and Wait: Thread giữ lock và đợi lock khác
  3. No Preemption: Lock không thể bị cướp đoạt
  4. Circular Wait: Thread 1 chờ Thread 2, Thread 2 chờ Thread 1

Cách phòng tránh Deadlock

1. Lock Ordering (Khuyến nghị)

// Luôn acquire locks theo thứ tự nhất định
private final Object lock1 = new Object();
private final Object lock2 = new Object();

public void method1() {
synchronized (lock1) { // Luôn lock1 trước
synchronized (lock2) {
// Work
}
}
}

public void method2() {
synchronized (lock1) { // Luôn lock1 trước (không đảo ngược)
synchronized (lock2) {
// Work
}
}
}

2. Timeout

// Sử dụng tryLock() với timeout (sẽ học ở bài Lock)
Lock lock1 = new ReentrantLock();
Lock lock2 = new ReentrantLock();

if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
// Work
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}

3. Avoid Nested Locks

// Tránh nested locks nếu có thể
public synchronized void transfer(Account to, int amount) {
this.balance -= amount;
to.deposit(amount); // Tránh synchronized trong synchronized
}

Phát hiện Deadlock: Thread Dumps và ThreadMXBean

1. Sử dụng jstack (Command-line tool)

# Lấy PID của Java process
jps

# Dump thread stack traces
jstack <PID>

# Hoặc trigger trong code
kill -3 <PID> # Unix/Linux
Ctrl + Break # Windows

Output khi có deadlock:

Found one Java-level deadlock:
=============================
"Thread-2":
waiting to lock monitor 0x00007f8b3c004e00 (object 0x00000007d5f0a0c0, a java.lang.Object),
which is held by "Thread-1"
"Thread-1":
waiting to lock monitor 0x00007f8b3c006350 (object 0x00000007d5f0a0d0, a java.lang.Object),
which is held by "Thread-2"

2. Phát hiện Deadlock trong Code với ThreadMXBean

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

public class DeadlockDetector {
private final Object lock1 = new Object();
private final Object lock2 = new Object();

public void method1() {
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + ": Holding lock1");
try { Thread.sleep(100); } catch (InterruptedException e) {}

synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + ": Acquired lock2");
}
}
}

public void method2() {
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + ": Holding lock2");
try { Thread.sleep(100); } catch (InterruptedException e) {}

synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + ": Acquired lock1");
}
}
}

// Monitor thread để phát hiện deadlock
public static void startDeadlockMonitor() {
Thread monitor = new Thread(() -> {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();

while (true) {
try {
Thread.sleep(3000); // Check mỗi 3 giây

long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null) {
System.out.println("\n🚨 DEADLOCK DETECTED!");
System.out.println("Number of deadlocked threads: " + deadlockedThreads.length);

ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(deadlockedThreads);
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("\nThread: " + threadInfo.getThreadName());
System.out.println("State: " + threadInfo.getThreadState());
System.out.println("Locked on: " + threadInfo.getLockName());
System.out.println("Locked by: " + threadInfo.getLockOwnerName());
System.out.println("\nStack trace:");
for (StackTraceElement element : threadInfo.getStackTrace()) {
System.out.println(" " + element);
}
}

// Có thể log hoặc alert
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Deadlock-Monitor");

monitor.setDaemon(true);
monitor.start();
}

public static void main(String[] args) {
DeadlockDetector detector = new DeadlockDetector();

// Bắt đầu deadlock monitor
startDeadlockMonitor();

// Tạo deadlock
Thread t1 = new Thread(() -> detector.method1(), "Thread-1");
Thread t2 = new Thread(() -> detector.method2(), "Thread-2");

t1.start();
t2.start();
}
}

Output:

Thread-1: Holding lock1
Thread-2: Holding lock2

🚨 DEADLOCK DETECTED!
Number of deadlocked threads: 2

Thread: Thread-1
State: BLOCKED
Locked on: java.lang.Object@1a2b3c4d
Locked by: Thread-2

Thread: Thread-2
State: BLOCKED
Locked on: java.lang.Object@5e6f7a8b
Locked by: Thread-1
Production Monitoring

Trong production, nên có:

  • Periodic thread dump: Chụp thread dumps định kỳ để phân tích
  • Monitoring alerts: Alert khi phát hiện deadlock
  • Health check endpoint: REST API trả về thread health status

Starvation và Livelock

Starvation

Thread không bao giờ được CPU do các threads khác có priority cao hơn:

Thread highPriority = new Thread(task);
highPriority.setPriority(Thread.MAX_PRIORITY);

Thread lowPriority = new Thread(task);
lowPriority.setPriority(Thread.MIN_PRIORITY); // Có thể bị starve

Livelock

Threads liên tục đổi trạng thái để tránh deadlock nhưng không tiến triển:

// 2 người nhường đường nhau liên tục → không ai đi được
while (otherThreadIsWaiting) {
// Yield to other thread
Thread.yield();
}

wait(), notify(), notifyAll()

Methods để inter-thread communication:

MethodMô tả
wait()Release lock và chờ cho đến khi được notify
notify()Đánh thức 1 thread đang chờ
notifyAll()Đánh thức tất cả threads đang chờ
cảnh báo
  • Phải gọi trong synchronized block/method
  • Gọi ngoài synchronized → IllegalMonitorStateException

Ví dụ: Producer-Consumer Pattern

import java.util.LinkedList;
import java.util.Queue;

public class ProducerConsumer {
private Queue<Integer> queue = new LinkedList<>();
private final int CAPACITY = 5;
private final Object lock = new Object();

public void produce() throws InterruptedException {
int value = 0;
while (true) {
synchronized (lock) {
// Chờ nếu queue đầy
while (queue.size() == CAPACITY) {
System.out.println("Queue full. Producer waiting...");
lock.wait(); // Release lock và chờ
}

// Produce
queue.add(value);
System.out.println("Produced: " + value);
value++;

// Notify consumer
lock.notify();

Thread.sleep(1000);
}
}
}

public void consume() throws InterruptedException {
while (true) {
synchronized (lock) {
// Chờ nếu queue rỗng
while (queue.isEmpty()) {
System.out.println("Queue empty. Consumer waiting...");
lock.wait(); // Release lock và chờ
}

// Consume
int value = queue.poll();
System.out.println("Consumed: " + value);

// Notify producer
lock.notify();

Thread.sleep(1500);
}
}
}

public static void main(String[] args) {
ProducerConsumer pc = new ProducerConsumer();

Thread producer = new Thread(() -> {
try {
pc.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
});

Thread consumer = new Thread(() -> {
try {
pc.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
});

producer.start();
consumer.start();
}
}

Output:

Produced: 0
Consumed: 0
Produced: 1
Produced: 2
Consumed: 1
Produced: 3
...
notify() vs notifyAll()
  • notify(): Đánh thức 1 thread (random) → hiệu suất tốt hơn
  • notifyAll(): Đánh thức tất cả threads → an toàn hơn (tránh missed signals)

Nên dùng notifyAll() nếu không chắc chắn.

Spurious Wakeups: Tại sao phải dùng while thay vì if

Spurious wakeup (đánh thức giả) là hiện tượng thread bị đánh thức không do notify()/notifyAll() mà do:

  • JVM implementation details
  • Operating system signals
  • Hardware interrupts

Vấn đề khi dùng if

// ❌ WRONG: Dùng if - có thể gây bug
public void consume() throws InterruptedException {
synchronized (lock) {
if (queue.isEmpty()) { // ← Chỉ check 1 lần!
lock.wait();
}

// Nếu spurious wakeup xảy ra, queue vẫn empty
// → poll() trả về null → NullPointerException!
int value = queue.poll();
System.out.println("Consumed: " + value);
}
}

Vấn đề:

  1. Thread wait vì queue empty
  2. Spurious wakeup → thread thức dậy
  3. Queue vẫn empty, nhưng thread tiếp tục thực thi
  4. queue.poll() trả về null → Bug!

Giải pháp: Luôn dùng while

// ✅ CORRECT: Dùng while - check lại condition sau khi thức
public void consume() throws InterruptedException {
synchronized (lock) {
while (queue.isEmpty()) { // ← Check lại sau khi thức!
lock.wait();
}

// Đảm bảo queue có data trước khi poll
int value = queue.poll();
System.out.println("Consumed: " + value);
}
}

JLS 17.2.1 khuyến cáo:

"A thread can also wake up without being notified, interrupted, or timing out, a so-called spurious wakeup. [...] waits should always occur in loops, like this:

synchronized (obj) {
while (<condition does not hold>)
obj.wait();
... // Perform action appropriate to condition
}

Ví dụ minh họa Bug

public class SpuriousWakeupDemo {
private final Object lock = new Object();
private boolean dataReady = false;

public void producer() throws InterruptedException {
Thread.sleep(2000);
synchronized (lock) {
dataReady = true;
System.out.println("Producer: Data ready, notifying...");
lock.notify();
}
}

// ❌ Bug version: Dùng if
public void consumerBuggy() throws InterruptedException {
synchronized (lock) {
if (!dataReady) { // ← if thay vì while
System.out.println("Consumer: Waiting for data...");
lock.wait();
}
// Nếu spurious wakeup: dataReady vẫn false!
if (dataReady) {
System.out.println("Consumer: Processing data");
} else {
System.out.println("Consumer: BUG! Data not ready but woke up!");
}
}
}

// ✅ Correct version: Dùng while
public void consumerCorrect() throws InterruptedException {
synchronized (lock) {
while (!dataReady) { // ← while: check lại sau spurious wakeup
System.out.println("Consumer: Waiting for data...");
lock.wait();
}
System.out.println("Consumer: Processing data");
}
}
}

wait() vs sleep(): Khác biệt quan trọng

wait()sleep()
Release lock✅ Có (release monitor, cơ chế giám sát đồng bộ)❌ Không (giữ lock)
Wake-upnotify() / notifyAll()Timeout hết
Must be synchronized✅ Bắt buộc❌ Không cần
ExceptionIllegalMonitorStateException nếu không synchronizedInterruptedException
public class WaitVsSleepDemo {
private final Object lock = new Object();

public void demonstrateWait() throws InterruptedException {
synchronized (lock) {
System.out.println("Before wait() - holding lock");
lock.wait(1000); // Release lock, thread khác có thể vào
System.out.println("After wait() - re-acquired lock");
}
}

public void demonstrateSleep() throws InterruptedException {
synchronized (lock) {
System.out.println("Before sleep() - holding lock");
Thread.sleep(1000); // KHÔNG release lock, thread khác bị block
System.out.println("After sleep() - still holding lock");
}
}

// ❌ Lỗi: wait() ngoài synchronized block
public void wrongWait() throws InterruptedException {
lock.wait(); // IllegalMonitorStateException!
}
}
Lưu ý
  • wait() phải gọi trong synchronized block
  • Luôn dùng while loop, không bao giờ dùng if
  • wait() release lock, sleep() KHÔNG release lock

Ví dụ thực tế: Thread-Safe BankAccount

public class ThreadSafeBankAccount {
private int balance;
private final Object lock = new Object();

public ThreadSafeBankAccount(int initialBalance) {
this.balance = initialBalance;
}

public void deposit(int amount) {
synchronized (lock) {
int newBalance = balance + amount;
// Giả lập processing time
try { Thread.sleep(10); } catch (InterruptedException e) {}
balance = newBalance;
System.out.println(Thread.currentThread().getName() +
" deposited " + amount + ", balance: " + balance);
}
}

public boolean withdraw(int amount) {
synchronized (lock) {
if (balance >= amount) {
int newBalance = balance - amount;
// Giả lập processing time
try { Thread.sleep(10); } catch (InterruptedException e) {}
balance = newBalance;
System.out.println(Thread.currentThread().getName() +
" withdrew " + amount + ", balance: " + balance);
return true;
} else {
System.out.println(Thread.currentThread().getName() +
": Insufficient balance");
return false;
}
}
}

public int getBalance() {
synchronized (lock) {
return balance;
}
}

public static void main(String[] args) throws InterruptedException {
ThreadSafeBankAccount account = new ThreadSafeBankAccount(1000);

// 5 threads cùng deposit
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++) {
final int amount = 100;
threads[i] = new Thread(() -> {
account.deposit(amount);
account.withdraw(50);
}, "Thread-" + i);
threads[i].start();
}

// Chờ tất cả threads hoàn thành
for (Thread t : threads) {
t.join();
}

System.out.println("\nFinal balance: " + account.getBalance());
// Expected: 1000 + (100*5) - (50*5) = 1250
}
}

Best Practices

Synchronization Best Practices
  1. Minimize scope: Synchronized block nhỏ nhất có thể
  2. Avoid nested locks: Giảm nguy cơ deadlock
  3. Lock ordering: Luôn acquire locks theo thứ tự nhất định
  4. Use notifyAll(): An toàn hơn notify() trong hầu hết trường hợp
  5. Don't lock on public objects: Dùng private final Object lock
  6. Don't call unknown code in synchronized: Có thể gây deadlock
  7. Prefer higher-level concurrency utilities: ExecutorService, Concurrent Collections

OCP Exam Tips

Oracle Certified Professional (OCP) Exam

Những điểm hay ra trong đề thi OCP về synchronization:

1. Synchronized Method Lock Object

Instance method:

public synchronized void method() { }
// Lock trên: this (instance object)

Static method:

public static synchronized void method() { }
// Lock trên: ClassName.class (Class object)

Chú ý: Instance method và static method KHÔNG chặn nhau (locks khác nhau)!

public class LockExample {
public synchronized void instanceMethod() { } // Lock: this
public static synchronized void staticMethod() { } // Lock: LockExample.class

// 2 threads có thể chạy song song:
// - Thread 1: instanceMethod() (lock on this)
// - Thread 2: staticMethod() (lock on LockExample.class)
}

2. volatile Không Đảm bảo Atomicity

// ❌ WRONG: volatile không giúp gì với compound operations
private volatile int count = 0;
public void increment() {
count++; // Race condition vẫn xảy ra!
}

// ✅ CORRECT: Dùng synchronized hoặc AtomicInteger
private int count = 0;
public synchronized void increment() {
count++;
}

Nhớ: volatile chỉ đảm bảo:

  • Visibility (tính khả kiến): Changes visible cho tất cả threads
  • Ordering: Ngăn chặn instruction reordering

Không đảm bảo:

  • Atomicity (tính nguyên tử): Compound operations như count++ không atomic

3. wait() Phải Trong synchronized Block

// ❌ WRONG: IllegalMonitorStateException
public void method() {
lock.wait(); // Crash!
}

// ✅ CORRECT: wait() trong synchronized
public void method() {
synchronized (lock) {
lock.wait(); // OK
}
}

Nhớ: wait(), notify(), notifyAll() phải gọi trong synchronized block/method của cùng object.

4. Deadlock: 4 Điều kiện Coffman

Deadlock xảy ra khi TẤT CẢ 4 điều kiện đúng:

  1. Mutual Exclusion: Resource không chia sẻ
  2. Hold and Wait: Thread giữ resource và đợi resource khác
  3. No Preemption: Không thể cướp resource
  4. Circular Wait: T1 → T2 → T1 (vòng tròn chờ đợi)

Cách phòng tránh: Phá vỡ ít nhất 1 trong 4 điều kiện trên (thường phá Circular Wait bằng lock ordering).

5. IllegalMonitorStateException

Exception này xảy ra khi:

  • Gọi wait(), notify(), notifyAll() ngoài synchronized block
  • Gọi trên object không phải object đang lock
Object lock1 = new Object();
Object lock2 = new Object();

synchronized (lock1) {
lock2.wait(); // ❌ IllegalMonitorStateException!
}

synchronized (lock1) {
lock1.wait(); // ✅ OK
}

6. Context Switching Overhead

Câu hỏi thi thường hỏi về overhead (chi phí phụ) của synchronization:

  • Context switching (chuyển đổi ngữ cảnh): CPU chuyển từ thread này sang thread khác
  • Lock contention: Threads chờ lock → CPU idle
  • Cache invalidation: CPU cache phải refresh

Tối ưu: Minimize synchronized block scope (càng nhỏ càng tốt).

7. Tricky Code Examples

// Câu hỏi: Có thread-safe không?
public class Counter {
private int count = 0;

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

public int getCount() { // ← Không synchronized!
return count;
}
}
// ❌ WRONG: getCount() có thể đọc stale value!
// ✅ CORRECT: synchronized cả getter và setter
// Câu hỏi: Deadlock có xảy ra không?
public synchronized void method1() {
method2(); // Gọi method khác
}

public synchronized void method2() {
// ...
}
// ✅ SAFE: Cùng object (this), cùng thread → Reentrant lock (OK)

Nhớ: Java locks là reentrant (có thể vào lại) — thread đã giữ lock có thể acquire lại cùng lock.

Bài tập

Bài 1: Race Condition Fix

Sửa class Counter sau để thread-safe:

public class Counter {
private int count = 0;
public void increment() { count++; }
public void decrement() { count--; }
public int getCount() { return count; }
}

Tạo 10 threads, mỗi thread increment 1000 lần. Verify kết quả = 10,000.

Bài 2: Deadlock Detection

Viết chương trình gây deadlock, sau đó sửa lại để tránh deadlock.

Bài 3: Producer-Consumer nâng cao

Mở rộng Producer-Consumer với:

  • Nhiều producers và consumers
  • Queue có capacity giới hạn
  • Thống kê số items produced/consumed

Bài 4: Thread-Safe Cache

Implement simple cache:

public class Cache {
public void put(String key, String value) { }
public String get(String key) { }
public void clear() { }
}

Đảm bảo thread-safe với synchronized.

Tóm tắt

  • Java Memory Model: CPU caches, reordering, happens-before relationships
  • synchronized: Đảm bảo mutual exclusion + visibility + happens-before
  • volatile: Đảm bảo visibility (tính khả kiến) + ordering, KHÔNG đảm bảo atomicity (tính nguyên tử)
  • Intrinsic Lock: Monitor lock trên object (this) hoặc Class object
  • Deadlock: 4 điều kiện Coffman, phòng tránh bằng lock ordering
  • wait/notify: Inter-thread communication, luôn dùng while loop
  • wait() vs sleep(): wait() release lock, sleep() KHÔNG release lock
  • Spurious wakeups: Tại sao phải dùng while thay vì if
  • Thread dumps: jstack, ThreadMXBean để phát hiện deadlock

Bài tiếp theo: Executor Framework - cách quản lý threads hiệu quả hơn!

Đọc thêm

Sách chuyên sâu

  • Java Concurrency in Practice (JCIP) - Chapter 3: Sharing Objects, Chapter 16: Java Memory Model

    • Cuốn sách "kinh thánh" về Java concurrency
    • Chi tiết về JMM, happens-before, publication patterns
  • Effective Java (3rd Edition) - Item 78: Synchronize access to shared mutable data

    • Best practices về synchronization
    • Khi nào dùng volatile vs synchronized

Tài liệu chính thức

Bài học liên quan

Tools

  • jstack: Command-line tool để dump thread stacks
  • VisualVM: GUI tool để monitor threads, detect deadlocks
  • JConsole: JMX-based monitoring tool