Giới thiệu Multithreading
Sau bài này, bạn sẽ:
- Hiểu được khái niệm Process, Thread và sự khác biệt giữa chúng
- Nắm được vòng đời (lifecycle) của một Thread trong Java
- Phân biệt được Concurrency và Parallelism
- Nhận biết được Race Condition và tác hại của nó
- Hiểu được các thách thức cơ bản khi lập trình đa luồng
Process vs Thread
Process
Process (tiến trình) là một chương trình đang chạy, có không gian bộ nhớ riêng biệt:
- Mỗi process có vùng nhớ heap, stack, code segment riêng
- Các process độc lập với nhau, không chia sẻ bộ nhớ
- Chi phí tạo và chuyển đổi giữa các process cao
- Giao tiếp giữa các process phức tạp (IPC - Inter-Process Communication)
Thread
Thread (luồng) là đơn vị thực thi nhỏ nhất trong một process:
- Nhiều threads trong cùng process chia sẻ heap memory
- Mỗi thread có stack riêng
- Chi phí tạo và chuyển đổi giữa threads thấp hơn process
- Giao tiếp giữa threads dễ dàng (shared memory)
Process
├── Thread 1 (có stack riêng)
├── Thread 2 (có stack riêng)
└── Thread 3 (có stack riêng)
└── Tất cả chia sẻ: Heap, Code, Data
Một Java application luôn có ít nhất 1 thread: main thread (thực thi method main()).
Tại sao cần Multithreading?
1. Tăng hiệu suất (Performance)
- Parallelism: Thực thi đồng thời trên nhiều CPU cores
- Concurrency: Tận dụng thời gian chờ I/O (file, network, database)
2. Responsive UI
- UI thread xử lý giao diện, worker threads xử lý tác vụ nặng
- Tránh ứng dụng bị "đơ" khi xử lý công việc lâu
3. Resource Sharing
- Threads chia sẻ bộ nhớ, giảm overhead so với processes
4. Scalability
- Tận dụng tốt hệ thống multi-core hiện đại
Amdahl's Law cho biết giới hạn của việc tăng tốc khi tăng số cores: Speedup = 1 / (S + P/N) trong đó S là phần tuần tự (serial), P là phần song song (parallel), N là số cores. Ngay cả với vô số cores, phần tuần tự vẫn giới hạn tốc độ tối đa.
Concurrency vs Parallelism
| Khái niệm | Ý nghĩa | Điều kiện |
|---|---|---|
| Concurrency | Nhiều tasks tiến triển đồng thời (interleaved - xen kẽ) | Có thể trên 1 core (context switching - chuyển đổi ngữ cảnh) |
| Parallelism | Nhiều tasks thực thi đồng thời thực sự | Cần nhiều cores |
Ví dụ:
- Concurrency: 1 người nấu ăn vừa luộc trứng, vừa chiên thịt (chuyển đổi liên tục)
- Parallelism: 2 người cùng nấu ăn, mỗi người một món
// Concurrency: Một CPU xử lý nhiều threads
Thread 1: ████░░░░████░░░░
Thread 2: ░░░░████░░░░████
Time: ────────────────→
// Parallelism: Nhiều CPUs xử lý cùng lúc
CPU 1 - Thread 1: ████████████████
CPU 2 - Thread 2: ████████████████
Time: ────────────────→
Thread Lifecycle
Một thread trong Java trải qua các trạng thái sau:
NEW
│
│ start() called
↓
RUNNABLE ←─────────────────────────────────┐
│ │
│ waiting for monitor lock │ lock acquired, notify(), notifyAll()
↓ │
BLOCKED ──────────────────────────────────┘
│ │
│ wait(), join(), LockSupport.park() │ notify(), notifyAll()
↓ │
WAITING ─────────────────────────────────→┘
│ │
│ sleep(), wait(timeout), join(timeout)│ timeout expired
↓ │
TIMED_WAITING ─────────────────────────────→──┘
│
│ run() completes or throws exception
↓
TERMINATED
Chú thích các chuyển đổi trạng thái:
- NEW → RUNNABLE: Gọi
start()khiến thread sẵn sàng chạy - RUNNABLE → BLOCKED: Thread chờ synchronized lock (preemptive scheduling - lập lịch ưu tiên)
- RUNNABLE → WAITING: Gọi
wait(),join(),LockSupport.park()- chờ vô thời hạn - RUNNABLE → TIMED_WAITING: Gọi
sleep(),wait(timeout),join(timeout)- chờ có giới hạn - WAITING/TIMED_WAITING → RUNNABLE:
notify(),notifyAll(), hoặc hết timeout - RUNNABLE → TERMINATED: Method
run()kết thúc hoặc ném exception
Các trạng thái chi tiết
1. NEW
Thread được tạo nhưng chưa gọi start()
Thread t = new Thread(() -> System.out.println("Hello"));
// t đang ở trạng thái NEW
2. RUNNABLE
Thread đã gọi start(), đang chạy hoặc sẵn sàng chạy (chờ CPU)
t.start(); // t chuyển sang RUNNABLE
Gọi start() chỉ làm thread sẵn sàng, không đảm bảo thread chạy ngay lập tức. Thread scheduler của JVM sử dụng time-slicing (chia lát thời gian) để quyết định khi nào thread được CPU.
3. BLOCKED
Thread đang chờ để có được monitor lock (synchronized)
synchronized (obj) {
// Thread khác đang giữ lock của obj
// Thread này sẽ ở trạng thái BLOCKED
}
4. WAITING
Thread chờ vô thời hạn cho đến khi thread khác thông báo
// Cách 1: Object.wait() (cần gọi trong synchronized block)
synchronized (obj) {
obj.wait(); // WAITING cho đến khi obj.notify()
}
// Cách 2: Thread.join()
t.join(); // WAITING cho đến khi thread t kết thúc
5. TIMED_WAITING
Thread chờ trong khoảng thời gian nhất định
Thread.sleep(1000); // TIMED_WAITING 1 giây
obj.wait(5000); // TIMED_WAITING tối đa 5 giây
t.join(2000); // TIMED_WAITING tối đa 2 giây
6. TERMINATED
Thread đã hoàn thành method run()
// Sau khi run() kết thúc, không thể start() lại thread
Race Conditions
Race condition xảy ra khi nhiều threads truy cập và thay đổi cùng một dữ liệu đồng thời, dẫn đến kết quả không mong muốn.
Vấn đề
public class Counter {
private int count = 0;
public void increment() {
count++; // KHÔNG thread-safe!
}
public int getCount() {
return count;
}
}
count++ không thread-safe?count++ thực chất là 3 bước:
- Đọc giá trị hiện tại:
temp = count - Tăng giá trị:
temp = temp + 1 - Ghi lại:
count = temp
Nếu 2 threads thực hiện đồng thời, có thể xảy ra:
count = 0
Thread 1: đọc count (0) → tính toán (1) → ghi (1)
Thread 2: đọc count (0) → tính toán (1) → ghi (1)
Kết quả: count = 1 (sai! đáng ra phải là 2)
Phân tích JVM Bytecode của count++
Để hiểu sâu hơn, hãy xem count++ được biên dịch thành bytecode như thế nào:
getfield count // Bước 1: Đọc giá trị count từ heap vào stack
iconst_1 // Bước 2: Đẩy hằng số 1 vào stack
iadd // Bước 3: Cộng hai giá trị trên stack
putfield count // Bước 4: Ghi kết quả từ stack về heap
Vấn đề: Giữa bất kỳ 2 bytecode nào, thread scheduler có thể chuyển sang thread khác (context switch). Ví dụ:
Thread 1: getfield count (đọc 0)
Thread 1: iconst_1
[CONTEXT SWITCH]
Thread 2: getfield count (đọc 0)
Thread 2: iconst_1
Thread 2: iadd (tính 0+1=1)
Thread 2: putfield count (ghi 1)
[CONTEXT SWITCH]
Thread 1: iadd (tính 0+1=1)
Thread 1: putfield count (ghi 1)
Kết quả: count = 1 (mất 1 lần tăng!)
Đây chính là lý do tại cấp độ JVM và phần cứng, count++ không phải là atomic operation (không nguyên tử).
Ví dụ đầy đủ: Race Condition
public class RaceConditionDemo {
private static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter++; // Race condition!
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter++; // Race condition!
}
});
t1.start();
t2.start();
t1.join(); // Chờ t1 kết thúc
t2.join(); // Chờ t2 kết thúc
System.out.println("Counter: " + counter);
// Kết quả không ổn định: có thể là 15234, 18992, 20000...
// Đáng ra phải là 20000!
}
}
Kết quả chạy nhiều lần
Run 1: Counter: 17843
Run 2: Counter: 19234
Run 3: Counter: 20000
Run 4: Counter: 18756
Run 5: Counter: 19999
Kết quả không ổn định! Đây là dấu hiệu của race condition. Mỗi lần chạy có thể cho kết quả khác nhau.
Ba thuộc tính của Concurrent Correctness
Để viết chương trình đa luồng chính xác, bạn cần đảm bảo 3 thuộc tính cơ bản. Hiểu 3 thuộc tính này là nền tảng để giải quyết MỌI vấn đề concurrency.
1. Atomicity (Tính nguyên tử)
Định nghĩa: Một operation được thực hiện hoàn toàn hoặc không thực hiện gì cả, không có trạng thái trung gian.
Vấn đề:
count++; // KHÔNG atomic! Gồm 3 bước: read, modify, write
Như đã phân tích ở trên, count++ có 4 bytecode instructions. Nếu thread bị preempt (bị gián đoạn) giữa các instruction, kết quả sẽ sai.
Ví dụ atomic operations:
int x = 10;- Gán giá trị (atomic trên primitive types ≤ 32-bit)AtomicInteger.incrementAndGet()- Atomic incrementsynchronizedblock - Toàn bộ block là atomic
Ví dụ NON-atomic operations:
count++,count--,count += 5long x = 123L;trên JVM 32-bit (cần 2 lần ghi)- Check-then-act:
if (x == 0) x = 1;
2. Visibility (Tính khả kiến)
Định nghĩa: Khi thread A thay đổi một biến, thread B phải thấy được thay đổi đó.
Vấn đề: Mỗi thread có CPU cache riêng. Thay đổi của thread A có thể chỉ nằm trong cache, chưa được flush về main memory.
public class VisibilityProblem {
private static boolean stop = false; // ⚠️ Không có visibility guarantee!
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
int count = 0;
while (!stop) { // Thread có thể cache giá trị stop = false
count++;
}
System.out.println("Stopped at: " + count);
});
worker.start();
Thread.sleep(1000);
stop = true; // Thay đổi này có thể KHÔNG visible cho worker thread!
System.out.println("Main set stop = true");
}
}
// Worker thread có thể chạy mãi mãi vì nó đọc cached value của stop!
Giải pháp:
volatile boolean stop- Đảm bảo read/write trực tiếp từ main memorysynchronized- Flush cache khi vào/ra blockAtomicBoolean- Visibility + atomicity
3. Ordering (Tính thứ tự)
Định nghĩa: Các instructions được thực thi theo thứ tự đúng như trong code, không bị hoán đổi (reorder).
Vấn đề: Compiler và CPU có thể reorder instructions để tối ưu hiệu suất, miễn là không thay đổi kết quả trong single-threaded context.
public class ReorderingProblem {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
a = 1; // Instruction 1
x = b; // Instruction 2
});
Thread t2 = new Thread(() -> {
b = 1; // Instruction 3
y = a; // Instruction 4
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("x=" + x + ", y=" + y);
}
}
// Kết quả có thể là: (0,0), (0,1), (1,0), hoặc (1,1)
// Nếu CPU reorder, có thể x=0 và y=0 xảy ra!
Giải pháp:
volatile- Ngăn reordering quanh volatile variablessynchronized- Thiết lập happens-before relationshipfinalfields - Đảm bảo được khởi tạo trước khi object visible
Tóm tắt 3 thuộc tính
| Thuộc tính | Vấn đề | Giải pháp chính |
|---|---|---|
| Atomicity | Operation bị gián đoạn giữa chừng | synchronized, AtomicXxx, locks |
| Visibility | Thread không thấy thay đổi của thread khác | volatile, synchronized |
| Ordering | Instructions bị reorder bởi compiler/CPU | volatile, synchronized, final |
Trong các bài tiếp theo (Synchronization, Volatile, Locks), chúng ta sẽ học cách giải quyết từng vấn đề này. Hãy luôn nhớ 3 thuộc tính này khi debug lỗi concurrency!
Tại sao cần học Multithreading?
1. Ứng dụng hiện đại cần concurrency
- Web servers: xử lý nhiều requests đồng thời
- Database systems: nhiều queries song song
- Desktop apps: responsive UI
- Games: render, AI, physics chạy song song
2. Hardware hiện đại là multi-core
- CPU hiện nay có 4, 8, 16+ cores
- Nếu chỉ dùng 1 thread → lãng phí tài nguyên
3. Cải thiện hiệu suất
// Tuần tự: 10 giây
processFile1(); // 5s
processFile2(); // 5s
// Song song: 5 giây
Thread t1 = new Thread(() -> processFile1());
Thread t2 = new Thread(() -> processFile2());
t1.start(); t2.start();
4. Các framework/library yêu cầu kiến thức multithreading
- Spring Boot: request handling
- Android: AsyncTask, Handler, Coroutines
- JavaFX: UI thread vs background tasks
Thách thức của Multithreading
- Race conditions: Dữ liệu không nhất quán
- Deadlocks: Threads chờ nhau mãi mãi
- Starvation: Thread không bao giờ được CPU
- Livelock: Threads liên tục đổi trạng thái nhưng không tiến triển
- Visibility issues: Thread không thấy thay đổi của thread khác
- Performance overhead: Context switching, synchronization
- Debugging khó: Lỗi không tái hiện được (non-deterministic)
Thread States:
- Biết rõ 6 trạng thái: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
start()vsrun(): Gọirun()trực tiếp KHÔNG tạo thread mới, chỉ chạy method bình thườngsleep()KHÔNG giải phóng lock (nếu đang trong synchronized block)
Thread Lifecycle:
- Gọi
start()2 lần trên cùng thread →IllegalThreadStateException - Một thread chỉ có thể
start()MỘT LẦN DUY NHẤT - Thread TERMINATED không thể restart
Thread Methods:
Thread.yield(): Chỉ là hint cho scheduler, có thể bị bỏ quajoin(): Thread hiện tại chờ thread khác kết thúcinterrupt(): Đánh dấu interrupted flag, không dừng thread ngay lập tức
Concurrency Issues:
count++KHÔNG phải atomic operation- Visibility problems: Dùng
volatilehoặcsynchronized - Race condition: Xảy ra khi nhiều threads truy cập shared mutable data mà không đồng bộ
Tóm tắt
| Khái niệm | Giải thích |
|---|---|
| Process | Chương trình đang chạy, có bộ nhớ riêng |
| Thread | Đơn vị thực thi trong process, chia sẻ bộ nhớ |
| Concurrency | Nhiều tasks tiến triển đồng thời (có thể trên 1 core) |
| Parallelism | Nhiều tasks thực thi đồng thời (cần nhiều cores) |
| Race Condition | Kết quả không xác định do nhiều threads truy cập shared data |
Bài tập
Bài 1: Thread States
Viết chương trình tạo thread và in ra trạng thái của nó sau mỗi thao tác:
- Sau khi new Thread()
- Sau khi start()
- Trong khi sleep()
- Sau khi hoàn thành
Bài 2: Race Condition
- Chạy
RaceConditionDemoở trên nhiều lần, ghi lại kết quả - Giải thích tại sao kết quả không giống nhau mỗi lần
- Dự đoán: Nếu tăng số lần loop lên 100,000 thì sai số sẽ tăng hay giảm?
Bài 3: Concurrency vs Parallelism
Viết chương trình:
- Tạo 4 threads, mỗi thread đếm từ 1 đến 1,000,000
- So sánh thời gian chạy trên máy có 2 cores vs 4 cores
- Có thấy sự khác biệt không? Tại sao?
Next Steps
Trong bài tiếp theo, chúng ta sẽ học cách tạo và quản lý threads với 3 cách:
- Extends Thread class
- Implements Runnable interface
- Implements Callable với Future
Và các methods quan trọng: start(), sleep(), join(), interrupt()...
Đọc thêm
Tài liệu chính thức:
- Oracle: Processes and Threads
- Oracle: Concurrency
- Java Language Specification Chapter 17: Threads and Locks
Sách tham khảo:
- Head First Java (3rd Edition) - Chapter 15: Make a Connection (Threads)
- Java Concurrency in Practice - Brian Goetz (Chapter 1-2)
Bài học tiếp theo:
- Tạo và quản lý Threads - Cách tạo threads với Thread, Runnable, Callable
- Synchronization - Giải quyết race conditions với synchronized, locks