Giải mã ReentrantLock: Cơ chế khóa đồng bộ tiên tiến trong Java

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())

  1. Thử lấy nhanh (Sửa đổi trạng thái bằng CAS)
  2. Nếu thất bại, vào hàng đợi CLH
  3. Sử dụng LockSupport.park() để tạm dừng luồng

Quy trình giải khóa (unlock())

  1. Sửa đổi trạng thái
  2. Đánh thức luồng tiếp theo trong hàng đợi
  3. 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

  1. Phải giải khóa:
khoa.lock();
try {
    // Vùng chỉ đọc
} finally {
    khoa.unlock(); // Phải đặt trong khối finally
}
  1. 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!
}
  1. 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
  1. 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;
}

Thẻ: Java Concurrency ReentrantLock AQS multithreading

Đăng vào ngày 17 tháng 7 lúc 05:52