99 Ngày Java — Ngày 66: Lock và deadlock
Sau Future, ta quay lại tài nguyên chia sẻ. synchronized giải quyết nhiều trường hợp, nhưng Lock cho phép thử lấy khóa và chờ có giới hạn. Đổi lại, người viết phải tự mở khóa ở mọi đường đi. Deadlock xuất hiện khi các luồng giữ khóa rồi chờ khóa của nhau mãi mãi.

1. ReentrantLock và finally
ReentrantLock cho cùng luồng lấy lại khóa mà nó đang giữ. lock() chờ đến khi có khóa; unlock() phải nằm trong finally, nhưng chỉ sau khi lock() đã thành công. Không đặt thao tác mạng chậm trong vùng khóa nếu không cần: nó giữ các luồng khác chờ lâu hơn.
private final Lock lock = new ReentrantLock();
void add() {
lock.lock();
try { count++; }
finally { lock.unlock(); }
}2. tryLock cho đường lui
tryLock(timeout, unit) chờ tối đa một khoảng thời gian và có thể bị interrupt. Chỉ unlock khi kết quả là true. Timeout giúp từ bỏ một lần thử, nhưng không tự làm cho toàn bộ thuật toán hết deadlock; cần xác định sẽ trả lỗi, thử lại hay xếp hàng khi không lấy được khóa.
if (!lock.tryLock(200, TimeUnit.MILLISECONDS)) {
throw new IllegalStateException("Resource busy");
}
try { update(); }
finally { lock.unlock(); }3. Deadlock hai khóa
Luồng A giữ khóa tài khoản một rồi chờ tài khoản hai; luồng B giữ khóa hai rồi chờ khóa một. Không luồng nào tiến tiếp. Phòng tránh bằng một thứ tự khóa toàn cục, chẳng hạn khóa theo accountId tăng dần, kể cả khi chiều chuyển tiền ngược lại. Cặp ID bằng nhau phải xử lý riêng.
if (a.id() == b.id()) return;
Account first = a.id() < b.id() ? a : b;
Account second = a.id() < b.id() ? b : a;
first.lock().lock();
try {
second.lock().lock();
try { transfer(a, b, amount); }
finally { second.lock().unlock(); }
} finally { first.lock().unlock(); }4. Chọn khóa và điều tra
Nếu chỉ cần tăng bộ đếm, cân nhắc AtomicInteger hoặc LongAdder; nếu cần nhiều thao tác nguyên tử cùng nhau, một khóa rõ ràng dễ kiểm chứng hơn. Giữ vùng khóa nhỏ và thứ tự lấy khóa nhất quán. Khi ứng dụng đứng, thread dump cho thấy các luồng BLOCKED hoặc WAITING và thường chỉ ra vòng chờ deadlock.
Bài tập: tạo hai luồng chuyển tiền ngược chiều, rồi sửa bằng thứ tự accountId.
Đo thời gian chờ khóa trước và sau khi đưa I/O ra khỏi vùng khóa.
Lock bảo vệ bất biến dữ liệu, nhưng cũng tạo rủi ro chờ vô hạn. Dùng finally, giới hạn thời gian khi phù hợp và một thứ tự khóa duy nhất. Ngày 67 dùng concurrent collections để giảm nhu cầu tự khóa.
Tài liệu đối chiếu
https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/locks/ReentrantLock.html
https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/locks/Lock.html
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


