Hiểu ReentrantLock qua ví dụ xếp hàng nhà vệ sinh
Hãy tưởng tượng một hệ thống nhà vệ sinh thông minh:
- Khóa synchronized: Chỉ có một bảng "Có người/Trống" (không công bằng, ai nhanh hơn sẽ vào trước).
- ReentrantLock: Hệ thống phát số + xếp hàng + làn VIP (có thể công bằng hoặc ưu tiên, xem được số người đang chờ).
ReentrantLock chính là một khóa đa năng như vậy, và bí mật nằm ở AQS (AbstractQueuedSynchronizer) phía sau nó!
Ba đặc tính chính của ReentrantLock
1. Khả năng tái nhập (Tái sử dụng khóa bởi cùng một luồng)
ReentrantLock khoa = new ReentrantLock();
void phuongThucA() {
khoa.lock();
try {
phuongThucB(); // Tái nhập
} finally {
khoa.unlock();
}
}
void phuongThucB() {
khoa.lock(); // Có thể tái nhập
try {
// ...
} finally {
khoa.unlock();
}
}
2. Lựa chọn tính công bằng (Công bằng vs Không công bằng)
// Khóa không công bằng (mặc định, hiệu suất cao)
ReentrantLock khoaKhongCongBang = new ReentrantLock();
// Khóa công bằng (đảm bảo đến trước phục vụ trước)
ReentrantLock khoaCongBang = new ReentrantLock(true);
3. Biến điều kiện (Đánh thức chính xác các luồng)
Condition dieuKien = khoa.newCondition();
// Luồng 1
khoa.lock();
try {
while (!dieuKienDuocThoaMan) {
dieuKien.await(); // Giải phóng khóa và đợi
}
} finally {
khoa.unlock();
}
// Luồng 2
khoa.lock();
try {
dieuKienDuocThoaMan = true;
dieuKien.signal(); // Đánh thức chính xác
} finally {
khoa.unlock();
}
Nguyên lý cơ bản: Ba yếu tố cốt lõi của AQS
1. Trạng thái State (Bộ đếm khóa)
// 0 nghĩa là chưa bị khóa, >0 nghĩa là số lần đã khóa
private volatile int trangThai;
2. Hàng đợi CLH (Hệ thống xếp hàng cho các luồng)
Đầu -> LuồngA(Đang giữ khóa) -> LuồngB(Đợi) -> LuồngC(Đợi) -> Cuối
3. Lấy/giải phóng logic (Thực hiện công bằng và không công bằng)
Thử lấy khóa không công bằng:
final boolean thuLayKhongCongBang(int lay) {
if (compareAndSetState(0, 1)) { // Cố gắng chen ngang
setChuSoHuuLuong(Thread.hienTai());
return true;
}
return false;
}
Thử lấy khóa công bằng:
protected final boolean thuLayCongBang(int lay) {
if (!coLuongDangDoi() && // Kiểm tra có luồng nào đang đợi
compareAndSetState(0, lay)) {
setChuSoHuuLuong(Thread.hienTai());
return true;
}
return false;
}
Phân tích toàn diện quy trình hoạt động
Quy trình khóa (lock())
- Thử lấy nhanh (Sửa đổi trạng thái bằng CAS)
- Nếu thất bại, vào hàng đợi CLH
- Sử dụng
LockSupport.park()để tạm dừng luồng
Quy trình giải khóa (unlock())
- Sửa đổi trạng thái
- Đánh thức luồng tiếp theo trong hàng đợi
- Luồng được đánh thức thử lấy khóa
So sánh với synchronized
| Tính năng | ReentrantLock | synchronized |
|---|---|---|
| **Cơ chế thực hiện** | AQS+Java code | JVM monitor nội tại |
| **Lấy khóa** | Có thể gián đoạn/hết giờ | Không thể gián đoạn |
| **Tính công bằng** | Có thể cấu hình công bằng/không công bằng | Hoàn toàn không công bằng |
| **Biến điều kiện** | Hỗ trợ nhiều Condition | Chỉ một hàng đợi chờ |
| **Hiệu suất** | Tốt hơn trong môi trường cạnh tranh cao | Tốt hơn trong môi trường cạnh tranh thấp |
| **Giải khóa** | Phải unlock() thủ công | Tự động giải khóa |
6 tình huống ứng dụng thực tế
1. Kiểm soát khóa chi tiết
void chuyenTien(TaiKhoan nguon, TaiKhoan dich, int soTien) {
nguon.khoa.lock();
try {
dich.khoa.lock();
try {
// Hoạt động chuyển tiền
} finally {
dich.khoa.unlock();
}
} finally {
nguon.khoa.unlock();
}
}
2. Thử lấy khóa (Tránh deadlock)
if (khoa.thuLay(1, TimeUnit.GIAY)) {
try {
// Hoạt động tài nguyên chia sẻ
} finally {
khoa.unlock();
}
} else {
log.warn("Thời gian lấy khóa hết");
}
3. Chờ nhiều điều kiện
class BoundedBuffer {
final ReentrantLock khoa = new ReentrantLock();
final Condition khongDay = khoa.newCondition();
final Condition khongTrong = khoa.newCondition();
void put(Object x) throws InterruptedException {
khoa.lock();
try {
while (soLuong == danhSach.length)
khongDay.await();
// Thao tác thêm vào hàng đợi
khongTrong.signal();
} finally {
khoa.unlock();
}
}
}
4. Giám sát trạng thái khóa
ReentrantLock khoa = new ReentrantLock();
// Lấy số lượng luồng đang chờ
int luongCho = khoa.getSoLuongHangDoi();
// Kiểm tra liệu khóa có đang được giữ bởi luồng hiện tại hay không
boolean dangDuocGiungBoiLuongHienTai = khoa.dangDuocGiungBoiLuongHienTai();
5. Bảo đảm thứ tự công bằng
// Đảm bảo yêu cầu xử lý theo thứ tự đến
ReentrantLock khoaCongBang = new ReentrantLock(true);
void xuLyYeuCau(YeuCau yeuCau) {
khoaCongBang.lock();
try {
// Xử lý yêu cầu
} finally {
khoaCongBang.unlock();
}
}
6. Nâng cấp khóa (Phối hợp với tryLock)
if (!khoa.dangDuocGiungBoiLuongHienTai() && khoa.thuLay()) {
try {
// Thành công lấy khóa
} finally {
khoa.unlock();
}
}
Hướng dẫn tối ưu hóa hiệu suất và tránh lỗi
- Phải giải khóa:
khoa.lock();
try {
// Vùng chỉ đọc
} finally {
khoa.unlock(); // Phải đặt trong khối finally
}
- Tránh lồng quá sâu:
// Số lần tái nhập quá nhiều ảnh hưởng hiệu suất
for (int i = 0; i < 1000; i++) {
khoa.lock(); // Nguy hiểm!
}
- Lựa chọn công bằng hợp lý:
- Khóa công bằng: Giảm thiểu đói nhưng hiệu suất thấp
- Khóa không công bằng: Hiệu suất cao nhưng có thể không công bằng
- Quy tắc sử dụng biến điều kiện:
while (!dieuKien) { // Phải dùng while thay vì if
cond.await();
}
Tiết lộ nguyên lý từ mã nguồn
Mã nguồn cốt lõi khóa (Phiên bản không công bằng):
final void lock() {
if (compareAndSetState(0, 1)) // Thử lấy nhanh
setChuSoHuuLuong(Thread.hienTai());
else
thuLay(1); // Vào hàng đợi AQS
}
Mã nguồn cốt lõi giải khóa:
protected final boolean thuGiaiKhoa(int giaiPhong) {
int c = getTrangThai() - giaiPhong;
if (Thread.hienTai() != getChuSoHuuLuong())
throw new IllegalMonitorStateException();
boolean daGiaiPhong = false;
if (c == 0) { // Đã giải phóng hoàn toàn
daGiaiPhong = true;
setChuSoHuuLuong(null);
}
setTrangThai(c); // Không cần CAS vì chỉ chủ sở hữu mới giải phóng
return daGiaiPhong;
}