1. Mutex: là gì và hoạt động ra sao
Mutex (từ “mutual exclusion” — “loại trừ lẫn nhau”) là cơ chế cho phép chỉ một luồng đồng thời thực thi vùng tới hạn của mã. Nếu mutex đang bị giữ (bị luồng khác chiếm), các luồng còn lại sẽ chờ cho đến khi nó được giải phóng.
Trong Java, vai trò mutex thường do đối tượng mà ta đồng bộ hóa trên đó đảm nhận: synchronized. Kể từ 5 của Java, xuất hiện lớp ReentrantLock — hiện thực mutex rõ ràng và linh hoạt hơn.
Minh họa
Hãy hình dung một căn phòng với một chiếc chìa khóa duy nhất (mutex). Để vào phòng, bạn cần lấy chìa. Nếu chìa không có (ai đó đã lấy), bạn chờ trước cửa. Khi chìa được trả về chỗ cũ (mutex được giải phóng), người tiếp theo có thể vào.
Cú pháp mutex trong Java
Qua synchronized (cổ điển):
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
Ở đây toàn bộ phương thức increment được bảo vệ bởi mutex — chỉ một luồng có thể thực thi nó tại một thời điểm.
Qua ReentrantLock (linh hoạt hơn):
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock(); // Chiếm mutex
try {
count++;
} finally {
lock.unlock(); // Nhất định phải giải phóng!
}
}
}
Quan trọng! Luôn giải phóng mutex trong khối finally, nếu không bạn có thể gặp “khóa vĩnh viễn” (deadlock) — và chương trình sẽ treo.
Khi nào cần mutex?
Mutex cần thiết khi tài nguyên phải được truy cập bởi chỉ một luồng tại một thời điểm. Đó có thể là một biến, một tệp hoặc cơ sở dữ liệu. Đặc biệt quan trọng khi thao tác với tài nguyên không nguyên tử: ngay cả count++ thực chất gồm ba bước — đọc giá trị, tăng và ghi lại. Không có mutex, nhiều luồng có thể chen vào giữa các bước và gây ra race condition.
2. Semaphore: để làm gì và hoạt động thế nào
Semaphore là “bộ điều tiết” cho phép nhiều luồng cùng làm việc với tài nguyên, nhưng không vượt quá số lượng đặt trước. Nếu đạt giới hạn, các luồng còn lại sẽ chờ đến lượt.
Tương tự: bãi đỗ dành cho 3 xe. Nếu tất cả chỗ đều đầy, xe đến sau sẽ chờ cho đến khi có xe rời đi.
Cú pháp semaphore trong Java
Để làm việc này dùng lớp Semaphore từ gói java.util.concurrent:
import java.util.concurrent.Semaphore;
public class ParkingLot {
private final Semaphore spots;
public ParkingLot(int places) {
this.spots = new Semaphore(places);
}
public void parkCar(String car) throws InterruptedException {
spots.acquire(); // Cố gắng chiếm chỗ (nếu không có thì chờ)
try {
System.out.println(car + " đã đỗ xe.");
Thread.sleep(1000); // Xe đang đỗ trong bãi
} finally {
spots.release(); // Trả chỗ
System.out.println(car + " đã rời đi.");
}
}
}
Sử dụng:
ParkingLot parking = new ParkingLot(3);
for (int i = 1; i <= 5; i++) {
final String car = "Xe " + i;
new Thread(() -> {
try {
parking.parkCar(car);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
Kết quả: cùng lúc sẽ không có quá ba chiếc xe trong bãi — các xe còn lại sẽ chờ.
Semaphore hoạt động như thế nào?
- Khi tạo semaphore, bạn đặt số “giấy phép” (permit).
- Phương thức acquire() cố gắng lấy giấy phép: nếu còn trống — luồng được đi tiếp, nếu không — chờ.
- Phương thức release() trả giấy phép về.
- Semaphore với một giấy phép cư xử gần như mutex, nhưng không có “chủ sở hữu”.
3. Mutex và semaphore: khác nhau ở đâu?
| Đặc điểm | Mutex | Semaphore |
|---|---|---|
| Số luồng | Chỉ một | Nhiều (số lượng giới hạn) |
| Mục đích | Bảo vệ tài nguyên | Giới hạn truy cập (ví dụ: pool) |
| API trong Java | |
|
| Quy tắc giải phóng | Thường là “chủ sở hữu” | Bất kỳ luồng nào cũng có thể giải phóng |
| Kịch bản điển hình | Bộ đếm chung, đối tượng | Pool kết nối, bãi đỗ, giới hạn |
- Mutex — dùng khi cần quyền truy cập độc quyền.
- Semaphore — dùng khi có thể cho phép nhiều luồng nhưng không phải tất cả.
Tương tự: mutex — nhà vệ sinh chỉ có một buồng; semaphore — nhà vệ sinh có ba buồng.
4. Ví dụ thực tế
Ví dụ 1: Mutex bảo vệ vùng tới hạn
Giả sử có một ngân hàng chung và nhiều luồng chuyển tiền giữa các tài khoản. Các thao tác phải mang tính nguyên tử.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BankAccount {
private int balance;
private final Lock lock = new ReentrantLock();
public BankAccount(int initial) {
this.balance = initial;
}
public void deposit(int amount) {
lock.lock();
try {
balance += amount;
} finally {
lock.unlock();
}
}
public void withdraw(int amount) {
lock.lock();
try {
if (balance >= amount) {
balance -= amount;
}
} finally {
lock.unlock();
}
}
public int getBalance() {
return balance;
}
}
Ở đây mọi thao tác với số dư đều được bảo vệ bằng mutex để tránh race condition.
Ví dụ 2: Semaphore để giới hạn truy cập
Máy chủ có thể đồng thời xử lý chỉ 2 khách (ví dụ do giấy phép).
import java.util.concurrent.Semaphore;
public class Server {
private final Semaphore connections = new Semaphore(2);
public void handleRequest(String client) throws InterruptedException {
connections.acquire();
try {
System.out.println(client + " đã kết nối tới máy chủ.");
Thread.sleep(2000); // Mô phỏng xử lý yêu cầu
} finally {
connections.release();
System.out.println(client + " đã ngắt kết nối.");
}
}
}
Sử dụng:
Server server = new Server();
for (int i = 1; i <= 5; i++) {
final String client = "Khách " + i;
new Thread(() -> {
try {
server.handleRequest(client);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
Kết quả: đồng thời máy chủ phục vụ không quá hai khách.
5. Đặc điểm và lưu ý khi sử dụng
Mutex: luôn giải phóng!
Rất quan trọng là không quên gọi unlock() (hoặc thoát khỏi khối đồng bộ) ngay cả khi có ngoại lệ. Hãy dùng try-finally:
lock.lock();
try {
// vùng tới hạn
} finally {
lock.unlock();
}
Nếu quên — có thể dẫn tới “khóa vĩnh viễn”; các luồng khác sẽ chờ vô hạn.
Semaphore: có thể giải phóng thay cho luồng khác không?
Khác với mutex, release() có thể được gọi bởi bất kỳ luồng nào, kể cả luồng không gọi acquire(). Điều này đôi khi tiện lợi, nhưng cũng dễ mắc lỗi — hãy tuân thủ kỷ luật.
Semaphore với một permit = mutex?
Gần như vậy. Nhưng semaphore không có khái niệm “chủ sở hữu”: bất kỳ thao tác giải phóng nào cũng tăng bộ đếm giấy phép. Với mutex, người giải phóng phải là người đã chiếm.
Đừng nhầm semaphore với pool
Semaphore không phải là pool đối tượng, nó chỉ là “bộ đếm giấy phép”. Nó thường được dùng để hiện thực các pool (ví dụ: pool kết nối đến DB), nhưng bản thân nó không lưu trữ gì.
6. Lỗi thường gặp khi làm việc với mutex và semaphore
Lỗi №1: Quên gọi unlock/release. Nếu bạn đã chiếm mutex hoặc semaphore nhưng không gọi unlock() hoặc release(), các luồng khác có thể bị treo vĩnh viễn. Luôn dùng try-finally để đảm bảo giải phóng khóa ngay cả khi có ngoại lệ.
Lỗi №2: Đồng bộ trên đối tượng sai. Nếu đồng bộ trên một biến không dùng chung cho tất cả các luồng (ví dụ: biến cục bộ hoặc literal chuỗi), đồng bộ sẽ không có tác dụng.
Lỗi №3: Giải phóng hai lần. Với semaphore: nếu gọi release() nhiều lần hơn acquire(), số giấy phép sẽ tăng vượt giới hạn. Hãy giữ cân bằng!
Lỗi №4: Dùng semaphore thay cho mutex (hoặc ngược lại). Nếu cần quyền truy cập độc quyền, hãy dùng mutex (synchronized hoặc Lock). Nếu cần giới hạn số luồng chạy đồng thời — hãy dùng Semaphore.
Lỗi №5: Giữ khóa quá lâu. Luồng giữ mutex hoặc semaphore càng lâu, các luồng khác phải chờ càng nhiều. Hãy tối thiểu hóa thời gian ở trong vùng tới hạn.
GO TO FULL VERSION