Synchronization
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
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;
}
}
}
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 đề:
- CPU Cache: Mỗi CPU có cache riêng → Thread 1 ghi vào cache, Thread 2 đọc từ cache cũ → stale data
- Store Buffer: CPU ghi vào buffer trước khi flush về memory → độ trễ visibility
- 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.
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 đọcreadytrong thread 2 - Do transitivity,
data = 42cũng happens-before đọcdata - → Thread 2 thấy cả
ready = truevàdata = 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
}
| Feature | volatile | synchronized |
|---|---|---|
| Visibility (tính khả kiến) | Có | Có |
| Atomicity (tính nguyên tử) | Chỉ với single read/write | Có (toàn bộ block) |
| Ordering (thứ tự) | Tạo happens-before edge | Tạo happens-before edge |
| Performance | Nhanh hơn (no locking overhead) | Chậm hơn (locking overhead, chi phí phụ) |
| Use case | Flags, status variables | Complex 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:
- Mutual Exclusion: Tài nguyên chỉ 1 thread sử dụng tại 1 thời điểm
- Hold and Wait: Thread giữ lock và đợi lock khác
- No Preemption: Lock không thể bị cướp đoạt
- 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
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:
| Method | Mô 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ờ |
- Phải gọi trong
synchronizedblock/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(): Đánh thức 1 thread (random) → hiệu suất tốt hơnnotifyAll(): Đá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 đề:
- Thread wait vì queue empty
- Spurious wakeup → thread thức dậy
- Queue vẫn empty, nhưng thread tiếp tục thực thi
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-up | notify() / notifyAll() | Timeout hết |
| Must be synchronized | ✅ Bắt buộc | ❌ Không cần |
| Exception | IllegalMonitorStateException nếu không synchronized | InterruptedException |
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!
}
}
wait()phải gọi trongsynchronizedblock- Luôn dùng
whileloop, không bao giờ dùngif 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
- Minimize scope: Synchronized block nhỏ nhất có thể
- Avoid nested locks: Giảm nguy cơ deadlock
- Lock ordering: Luôn acquire locks theo thứ tự nhất định
- Use notifyAll(): An toàn hơn notify() trong hầu hết trường hợp
- Don't lock on public objects: Dùng private final Object lock
- Don't call unknown code in synchronized: Có thể gây deadlock
- Prefer higher-level concurrency utilities: ExecutorService, Concurrent Collections
OCP Exam Tips
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:
- Mutual Exclusion: Resource không chia sẻ
- Hold and Wait: Thread giữ resource và đợi resource khác
- No Preemption: Không thể cướp resource
- 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
whileloop - wait() vs sleep(): wait() release lock, sleep() KHÔNG release lock
- Spurious wakeups: Tại sao phải dùng
whilethay 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
-
Java Language Specification (JLS) 17.4 - Memory Model
- Định nghĩa chính thức về JMM
- Happens-before rules (JLS 17.4.5)
- Wait sets và notification (JLS 17.2.1)
-
Oracle Tutorial: Synchronization
- Hướng dẫn cơ bản về synchronized, volatile, wait/notify
- Ví dụ Producer-Consumer
-
Oracle Tutorial: Memory Consistency Errors
- Giải thích visibility problems
- Happens-before relationship
Bài học liên quan
- Thread Creation - Tạo threads với Thread, Runnable, Callable
- Executor Framework - Thread pools và task management
- Concurrent Collections - Thread-safe collections
- Locks và Conditions - Lock-free thread-safe operations
Tools
- jstack: Command-line tool để dump thread stacks
- VisualVM: GUI tool để monitor threads, detect deadlocks
- JConsole: JMX-based monitoring tool