Trong lập trình đa luồng Java, việc đảm bảo tính toàn vẹn dữ liệu khi nhiều luồng cùng truy cập một tài nguyên chia sẻ là vô cùng quan trọng. Java cung cấp hai cơ chế chính để đạt được điều này: từ khóa synchronized và interface Lock. Bài viết này sẽ đi sâu vào cách thức hoạt động, ứng dụng và sự khác biệt giữa hai cơ chế này.
1. Giới thiệu về Synchronized
synchronized là một từ khóa cơ bản trong Java, được tích hợp sẵn để quản lý quyền truy cập của các luồng vào các khối mã hoặc phương thức quan trọng, ngăn chặn tình trạng race condition (điều kiện tranh chấp) và đảm bảo tính đồng bộ.
1.1. Các dạng áp dụng của từ khóa synchronized
Từ khóa synchronized có thể được áp dụng theo nhiều cách khác nhau, mỗi cách có phạm vi khóa riêng:
- Khóa một khối mã: Khi áp dụng cho một khối mã (ví dụ:
synchronized (object) { ... }), nó khóa đối tượng được chỉ định trong ngoặc đơn. Chỉ một luồng được phép thực thi khối mã bên trong của đối tượng đó tại một thời điểm. - Khóa một phương thức thực thể: Khi khai báo trên một phương thức không tĩnh (ví dụ:
public synchronized void myMethod()), nó khóa đối tượng hiện tại (this) của lớp. Điều này có nghĩa là chỉ một luồng có thể gọi bất kỳ phương thứcsynchronizednào của cùng một đối tượng tại một thời điểm. - Khóa một phương thức tĩnh: Khi áp dụng cho một phương thức tĩnh (ví dụ:
public static synchronized void staticMethod()), nó khóa đối tượngClasscủa lớp đó. Điều này đảm bảo rằng chỉ một luồng duy nhất có thể thực thi bất kỳ phương thức tĩnhsynchronizednào của lớp đó tại một thời điểm, trên tất cả các thể hiện của lớp.
Một điểm cần lưu ý là synchronized không phải là một phần của chữ ký phương thức và do đó không được kế thừa. Nếu một phương thức synchronized ở lớp cha được ghi đè ở lớp con, phương thức ở lớp con sẽ không tự động đồng bộ hóa. Bạn phải thêm từ khóa synchronized một cách tường minh vào phương thức của lớp con nếu muốn duy trì tính đồng bộ.
1.2. Ví dụ minh họa với synchronized: Hệ thống bán hàng
Hãy xem xét một hệ thống bán hàng đơn giản, nơi nhiều người bán hàng cùng lúc cố gắng bán sản phẩm từ một kho hàng chung. Nếu không có cơ chế đồng bộ hóa, có thể xảy ra tình trạng bán quá số lượng sản phẩm hoặc số liệu tồn kho bị sai lệch.
package com.example.concurrency.sync;
class ProductInventory {
private int stockCount = 30; // Số lượng sản phẩm có sẵn trong kho
public synchronized void sellProduct() {
if (stockCount > 0) {
System.out.println(Thread.currentThread().getName() +
": Đã bán một sản phẩm. Số lượng còn lại: " + (--stockCount));
} else {
System.out.println(Thread.currentThread().getName() + ": Hết hàng.");
}
}
}
public class StoreSales {
public static void main(String[] args) {
ProductInventory inventory = new ProductInventory();
// Tạo ba luồng đại diện cho ba người bán hàng
new Thread(() -> {
for (int i = 0; i < 40; i++) { // Mỗi người bán thử bán 40 lần
inventory.sellProduct();
}
}, "SellerA").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
inventory.sellProduct();
}
}, "SellerB").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
inventory.sellProduct();
}
}, "SellerC").start();
}
}
Trong ví dụ trên, phương thức sellProduct() được đánh dấu synchronized, đảm bảo rằng chỉ có một luồng (người bán) được phép thực thi khối mã bán hàng tại một thời điểm, tránh các vấn đề về đồng bộ hóa.
2. Khám phá Interface Lock
Interface Lock, được giới thiệu trong Java 5 cùng với gói java.util.concurrent.locks, cung cấp một phương pháp kiểm soát việc khóa linh hoạt và mạnh mẽ hơn nhiều so với từ khóa synchronized. Nó mang đến cho lập trình viên khả năng kiểm soát chi tiết hơn đối với các thao tác khóa, bao gồm cả việc thử khóa không chặn, khóa có thời gian chờ, và khả năng phản hồi ngắt.
2.1. Điểm khác biệt cơ bản giữa Lock và synchronized
- Nguồn gốc:
synchronizedlà một từ khóa được tích hợp sẵn trong ngôn ngữ Java, do JVM quản lý hoàn toàn. Ngược lại,Locklà một interface và được triển khai thông qua các lớp cụ thể nhưReentrantLock, mang lại khả năng kiểm soát khóa ở cấp độ lập trình. - Giải phóng khóa:
synchronizedtự động giải phóng khóa khi khối mã hoặc phương thức hoàn tất thực thi (bình thường hoặc do ngoại lệ). VớiLock, bạn phải tự tay gọi phương thứcunlock()để giải phóng khóa. Việc quên giải phóng khóa có thể dẫn đến tình trạng deadlock (tắc nghẽn) nghiêm trọng, khiến các luồng khác bị chặn vĩnh viễn. Do đó, việc sử dụng khốitry-finallylà bắt buộc khi làm việc vớiLockđể đảm bảo khóa luôn được giải phóng.
2.2. Ví dụ minh họa với Lock: Quản lý kho hàng
Chúng ta sẽ tái hiện ví dụ về hệ thống bán hàng, nhưng lần này sử dụng ReentrantLock để quản lý truy cập kho hàng.
package com.example.concurrency.lock;
import java.util.concurrent.locks.ReentrantLock;
class WarehouseInventory {
private int itemCount = 30; // Tổng số mặt hàng trong kho
private final ReentrantLock inventoryLock = new ReentrantLock(); // Khóa để bảo vệ truy cập
public void reduceItemCount() {
inventoryLock.lock(); // Yêu cầu khóa
try {
if (itemCount > 0) {
System.out.println(Thread.currentThread().getName() +
": Đã xuất kho một mặt hàng. Số lượng còn lại: " + (--itemCount));
} else {
System.out.println(Thread.currentThread().getName() + ": Kho đã hết hàng.");
}
} finally {
inventoryLock.unlock(); // Đảm bảo giải phóng khóa ngay cả khi có lỗi
}
}
}
public class WarehouseOperations {
public static void main(String[] args) {
WarehouseInventory inventory = new WarehouseInventory();
// Tạo các luồng mô phỏng công nhân xuất hàng
new Thread(() -> {
for (int i = 0; i < 40; i++) {
inventory.reduceItemCount();
}
}, "WorkerX").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
inventory.reduceItemCount();
}
}, "WorkerY").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
inventory.reduceItemCount();
}
}, "WorkerZ").start();
}
}
Trong ví dụ này, phương thức reduceItemCount() sử dụng một đối tượng ReentrantLock. Khóa được lấy bằng lock() và đảm bảo được giải phóng trong khối finally bằng unlock().
2.3. So sánh chi tiết Lock và synchronized
Ngoài những điểm khác biệt cơ bản đã nêu, Lock và synchronized còn có những điểm khác biệt sâu hơn:
- Bản chất và quản lý:
synchronizedlà một từ khóa được JVM quản lý trực tiếp, đơn giản và dễ sử dụng.Locklà một interface với các lớp triển khai cụ thể, cung cấp khả năng kiểm soát lập trình cao hơn nhưng đòi hỏi quản lý thủ công.
- Xử lý ngoại lệ và giải phóng khóa:
synchronized: JVM tự động giải phóng khóa khi khối mã kết thúc, dù là bình thường hay do ngoại lệ, giúp ngăn chặn deadlock.Lock: Yêu cầu lập trình viên phải gọiunlock()một cách tường minh, thường là trong khốifinally, để đảm bảo khóa được giải phóng. Nếu không, có thể dẫn đến deadlock.
- Khả năng phản hồi ngắt:
synchronized: Một luồng đang chờ khóasynchronizedsẽ tiếp tục chờ cho đến khi có thể lấy được khóa mà không phản ứng với các tín hiệu ngắt (interrupt).Lock: Cung cấp phương thứclockInterruptibly(), cho phép một luồng đang chờ khóa có thể bị ngắt và thoát khỏi trạng thái chờ.
- Thăm dò khóa và thời gian chờ:
synchronized: Không có cơ chế nào để kiểm tra xem khóa có sẵn hay không, hoặc để thử lấy khóa với một thời gian chờ giới hạn. Luồng sẽ chặn vô thời hạn.Lock: Cung cấp các phương thức nhưtryLock()(thử lấy khóa không chặn, trả vềtruenếu thành công) vàtryLock(long timeout, TimeUnit unit)(thử lấy khóa với thời gian chờ), cho phép lập trình viên kiểm soát cách luồng tương tác với khóa.
- Hiệu suất đọc và linh hoạt:
synchronized: Chỉ cung cấp một loại khóa độc quyền, nghĩa là mọi truy cập (đọc hoặc ghi) đều phải đợi.Lock: Với các triển khai nhưReentrantReadWriteLock, nó có thể cung cấp hiệu suất tốt hơn đáng kể cho các hoạt động đọc đồng thời, cho phép nhiều luồng đọc cùng lúc nhưng chỉ một luồng ghi tại một thời điểm.
- Hiệu suất dưới tải nặng:
- Trong các tình huống cạnh tranh tài nguyên thấp, hiệu suất của
synchronizedvàLockthường tương đương. - Tuy nhiên, khi cạnh tranh cao (nhiều luồng tranh giành cùng một tài nguyên),
Lockthường thể hiện hiệu suất vượt trội hơn nhờ khả năng điều khiển chi tiết và các tối ưu hóa nội bộ của nó.
- Trong các tình huống cạnh tranh tài nguyên thấp, hiệu suất của