Khi làm việc trong môi trường đa luồng, từ khóa synchronized thường được sử dụng để đồng bộ và đảm bảo an toàn cho luồng. Tuy nhiên, Lock API mang đến phương pháp linh hoạt hơn trong việc quản lý truy cập tài nguyên. Bài viết này sẽ hướng dẫn bạn cách sử dụng ReentrantLock trong Java để thay thế synchronized một cách hiệu quả.

Lock API trong Java
Trong hầu hết các trường hợp, từ khóa synchronized là lựa chọn phù hợp. Tuy nhiên, nó có một vài nhược điểm cố hữu, dẫn đến sự ra đời của Lock API trong gói Java Concurrency.
Kể từ Java 1.5, Concurrency API đã cung cấp gói java.util.concurrent.locks với các interface và class triển khai nhằm cải thiện cơ chế khóa đối tượng trong Java. Một số interface và class quan trọng trong Lock API bao gồm:
- Lock: Đây là interface cơ bản của Lock API. Nó cung cấp mọi tính năng của từ khóa
synchronized, đồng thời bổ sung nhiều cách khác như tạo các Condition (điều kiện) khác nhau để khóa, hay cung cấp cơ chế timeout (thời gian chờ tối đa cho phép) để một luồng không phải chờ lock vô tận. Một số phương thức quan trọng bao gồmlock()để chiếm giữ lock,unlock()để nhả lock,tryLock()để thử chờ lock trong một khoảng thời gian nhất định, vànewCondition()để tạo Condition. - Condition: Các đối tượng Condition ở đây tương tự như mô hình wait-notify của Object, nhưng được bổ sung tính năng tạo ra nhiều tập hợp chờ (wait-set) riêng biệt. Một đối tượng Condition luôn được tạo ra từ một đối tượng Lock. Các phương thức quan trọng của nó bao gồm
await()(tương tựwait()),signal()vàsignalAll()(tương tựnotify()vànotifyAll()). - ReadWriteLock: Interface này quản lý một cặp lock có liên quan với nhau: một cho các tác vụ chỉ đọc (read-only) và một cho tác vụ ghi. Lock để đọc có thể được chiếm giữ đồng thời bởi nhiều luồng, miễn là không có luồng nào đang giữ lock ghi. Ngược lại, lock ghi là độc quyền, chỉ có tối đa một luồng nắm giữ.
- ReentrantLock: Đây là class triển khai phổ biến nhất của interface Lock. Class này triển khai Lock theo cách tương tự như cơ chế của từ khóa synchronized. Ngoài các phương thức của interface Lock, ReentrantLock còn cung cấp một số phương thức tiện ích để lấy thông tin về luồng đang giữ lock, các luồng đang chờ lock, v.v. Các khối synchronized vốn đã có tính chất “reentrant” (tái nhập): nếu một luồng đã chiếm giữ lock của một monitor object (đối tượng kiểm soát), nó có thể đi vào một khối synchronized khác cũng yêu cầu lock trên chính monitor object đó. Đây có lẽ là lý do class này được đặt tên là ReentrantLock. Ta hãy xem một ví dụ đơn giản để hiểu rõ hơn về tính năng này.
public class Test{
public synchronized foo(){
//do something
bar();
}
public synchronized bar(){
//do some more
}
}
Nếu một luồng đi vào phương thức foo(), nó sẽ chiếm giữ lock của đối tượng Test. Do đó, khi luồng này tiếp tục gọi đến phương thức bar(), nó vẫn được phép chạy vì nó đã có sẵn lock trên đối tượng Test đó. Cơ chế này tương tự như synchronized(this).
Ví dụ sử dụng ReentrantLock trong Java
Bây giờ, hãy cùng xem một ví dụ đơn giản về cách thay thế từ khóa synchronized bằng Java Lock API. Giả sử ta có một class Resource với một số tác vụ cần đảm bảo an toàn luồng trong khi một số phương thức khác không yêu cầu điều này.
package com.journaldev.threads.lock;
public class Resource {
public void doSomething(){
//do some operation, DB read, write etc
}
public void doLogging(){
//logging, no need for thread safety
}
}
Tiếp theo, giả sử ta có một class Runnable sẽ sử dụng các phương thức của Resource.
package com.journaldev.threads.lock;
public class SynchronizedLockExample implements Runnable{
private Resource resource;
public SynchronizedLockExample(Resource r){
this.resource = r;
}
@Override
public void run() {
synchronized (resource) {
resource.doSomething();
}
resource.doLogging();
}
}
Lưu ý rằng ta đang dùng khối synchronized để chiếm lock trên đối tượng Resource. Ta cũng có thể tạo một object “giả” (dummy object) trong class và dùng nó cho mục đích khóa.
Bây giờ, hãy xem cách ta có thể sử dụng Lock API và viết lại chương trình trên mà không cần đến synchronized. Cụ thể, ta sẽ dùng ReentrantLock.
package com.journaldev.threads.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConcurrencyLockExample implements Runnable{
private Resource resource;
private Lock lock;
public ConcurrencyLockExample(Resource r){
this.resource = r;
this.lock = new ReentrantLock();
}
@Override
public void run() {
try {
if(lock.tryLock(10, TimeUnit.SECONDS)){
resource.doSomething();
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
//release lock
lock.unlock();
}
resource.doLogging();
}
}
Ở đây ta sử dụng phương thức tryLock() để đảm bảo luồng chỉ chờ trong một khoảng thời gian hữu hạn. Nếu không chiếm được lock, luồng sẽ chỉ ghi log và kết thúc. Một điểm quan trọng khác cần lưu ý là việc sử dụng khối try-finally để đảm bảo lock luôn được nhả ra, ngay cả khi phương thức doSomething() có lỗi và tạo ra exception.
So sánh giữa Lock và synchronized
Dựa trên các chi tiết và ví dụ ở trên, ta có thể dễ dàng rút ra những điểm khác biệt sau giữa Java Lock và synchronized.
- Java Lock API cung cấp nhiều tùy chọn và khả năng kiểm soát việc khóa hơn. Khác với từ khóa
synchronizedcó thể khiến một luồng phải chờ lock vô thời hạn, vớiLock, ta có thể dùngtryLock()để đảm bảo luồng chỉ chờ trong một khoảng thời gian nhất định. - Code sử dụng
synchronizedgọn gàng và dễ bảo trì hơn. Trong khi đó, vớiLock, ta bắt buộc phải dùng khốitry-finallyđể đảm bảo lock được giải phóng ngay cả khi có exception xảy ra giữa lệnh gọilock()vàunlock(). synchronizedchỉ có thể áp dụng trong phạm vi một phương thức hoặc một khối lệnh. Ngược lại, với Lock API, ta có thể chiếm giữ lock ở một phương thức và nhả nó ra ở một phương thức khác.- Từ khóa
synchronizedkhông đảm bảo tính công bằng (fairness). Trong khi đó, khi tạo đối tượng ReentrantLock, ta có thể thiết lập fairness thànhtrueđể luồng nào chờ lâu nhất sẽ được ưu tiên nhận lock trước. - Ta có thể tạo ra nhiều Condition khác nhau từ một Lock, cho phép các luồng riêng biệt có thể
await()trên các điều kiện khác nhau đó.
Tổng kết
Qua bài viết, bạn đã tìm hiểu cách sử dụng ReentrantLock trong Java như một giải pháp linh hoạt thay thế cho synchronized trong lập trình đa luồng. Kiến thức này giúp bạn kiểm soát khóa hiệu quả hơn, giảm thiểu tình trạng chờ vô thời hạn và đảm bảo an toàn khi truy cập tài nguyên chia sẻ.