Trang chủHướng dẫnVí dụ sử dụng ReentrantLock trong Java
Java

Ví dụ sử dụng ReentrantLock trong Java

CyStack blog 5 phút để đọc
CyStack blog22/07/2025
Locker Avatar

Bao Tran

Web Developer

Locker logo social
Reading Time: 5 minutes

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ả.

ReentrantLock trong Java

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:

  1. 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ồm lock() để 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.
  2. 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()signalAll() (tương tự notify()notifyAll()).
  3. 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ữ.
  4. 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.

  1. 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 synchronized có thể khiến một luồng phải chờ lock vô thời hạn, với Lock, ta có thể dùng tryLock() để đảm bảo luồng chỉ chờ trong một khoảng thời gian nhất định.
  2. Code sử dụng synchronized gọn gàng và dễ bảo trì hơn. Trong khi đó, với Lock, ta bắt buộc phải dùng khối try-finally để đảm bảo lock được giải phóng ngay cả khi có exception xảy ra giữa lệnh gọi lock()unlock().
  3. synchronized chỉ 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.
  4. Từ khóa synchronized khô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ành true để luồng nào chờ lâu nhất sẽ được ưu tiên nhận lock trước.
  5. 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ẻ.

0 Bình luận

Đăng nhập để thảo luận

Chuyên mục Hướng dẫn

Tổng hợp các bài viết hướng dẫn, nghiên cứu và phân tích chi tiết về kỹ thuật, các xu hướng công nghệ mới nhất dành cho lập trình viên.

Đăng ký nhận bản tin của chúng tôi

Hãy trở thành người nhận được các nội dung hữu ích của CyStack sớm nhất

Xem chính sách của chúng tôi Chính sách bảo mật.

Đăng ký nhận Newsletter

Nhận các nội dung hữu ích mới nhất