Loại trừ lẫn nhau
(Mutual Exclusion)
Loại trừ lẫn nhau là một cơ chế để đảm bảo rằng chỉ một luồng có thể truy cập và thay đổi tài nguyên chia sẻ tại một thời điểm, nhằm ngăn chặn các luồng can thiệp vào nhau và gây ra các vấn đề về tính đồng nhất dữ liệu. Điều này được thực hiện bằng cách kiểm soát quyền truy cập vào các tài nguyên chia sẻ. Trong Java, loại trừ lẫn nhau có thể được thực hiện bằng các cơ chế sau: Phương thức đồng bộ hóa (Synchronized Method), khối đồng bộ hóa (Synchronized Block) và đồng bộ hóa tĩnh (Static Synchronization).
1. Phương thức đồng bộ hóa (Synchronized Method)
↳ Khái niệm: Phương thức đồng bộ hóa là một phương thức mà chỉ cho phép một luồng duy nhất truy cập vào nó tại một thời điểm. Điều này đảm bảo rằng tài nguyên chia sẻ được truy cập một cách an toàn.
↳ Cách sử dụng: Để đồng bộ hóa một phương thức, bạn chỉ cần thêm từ khóa synchronized vào định nghĩa phương thức:
Ví dụ
public synchronized void synchronizedMethod() {
// mã thực thi an toàn với nhiều luồng
}
↳ Ưu điểm:
↳ Đơn giản: Dễ dàng sử dụng và hiểu. Chỉ cần thêm từ khóa synchronized vào định nghĩa phương thức.
↳ Bảo vệ tài nguyên chia sẻ: Đảm bảo rằng chỉ một luồng có thể thực thi phương thức tại một thời điểm, giúp ngăn ngừa sự can thiệp của các luồng khác và đảm bảo tính nhất quán của dữ liệu.
↳ Nhược điểm:
↳ Hiệu suất kém: Có thể làm giảm hiệu suất nếu phương thức đồng bộ hóa không cần thiết, vì tất cả các luồng phải đợi nhau để thực thi phương thức.
↳ Khóa toàn bộ phương thức: Đồng bộ hóa toàn bộ phương thức có thể không cần thiết và không hiệu quả nếu chỉ một phần của phương thức cần đồng bộ hóa.
Dưới đây là phiên bản đồng bộ hóa của phương thức increment() trong ví dụ trước:
Ví dụ: Counter.java
class Counter {
private int count = 0;
// Phương thức tăng giá trị của biến đếm, đã đồng bộ hóa
public synchronized void increment() {
count++;
}
// Phương thức lấy giá trị của biến đếm
public int getCount() {
return count;
}
public static void main(String[] args) {
Counter counter = new Counter();
// Tạo hai luồng, mỗi luồng sẽ thực hiện phương thức increment
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
// Bắt đầu hai luồng
t1.start();
t2.start();
// Chờ các luồng kết thúc
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// In ra giá trị cuối cùng của biến đếm
System.out.println("Final count: " + counter.getCount());
}
}
Kết quả của chương trình là
Với đồng bộ hóa, các luồng sẽ không can thiệp vào nhau khi thực hiện thao tác trên biến count. Do đó, giá trị cuối cùng của count sẽ chính xác và bằng 2000 sau khi tất cả các luồng hoàn thành công việc của chúng.
2. Khối đồng bộ hóa (Synchronized Block)
↳ Khái niệm: Khối đồng bộ hóa là một phần của phương thức được đồng bộ hóa. Nó cho phép bạn đồng bộ hóa chỉ một phần cụ thể của phương thức thay vì toàn bộ phương thức.
↳ Cách sử dụng: Để tạo khối đồng bộ hóa, bạn dùng từ khóa synchronized với một đối tượng khóa:
Ví dụ
public void method() {
// Một số mã không cần đồng bộ hóa
synchronized (lockObject) {
// mã thực thi an toàn với nhiều luồng
}
// Một số mã không cần đồng bộ hóa
}
↳ Ưu điểm:
↳ Tinh chỉnh: Cho phép đồng bộ hóa chỉ phần mã cần thiết, thay vì toàn bộ phương thức, giúp cải thiện hiệu suất và giảm phạm vi khóa.
↳ Tăng cường hiệu suất: Đưa ra sự linh hoạt trong việc lựa chọn đối tượng khóa và phần mã cần đồng bộ hóa, giúp giảm sự cạnh tranh tài nguyên.
↳ Nhược điểm:
↳ Phức tạp hơn: Có thể phức tạp hơn so với phương thức đồng bộ hóa vì phải quản lý đối tượng khóa và xác định chính xác phần mã cần đồng bộ hóa.
↳ Rủi ro khóa lồng ghép: Có thể dẫn đến tình trạng khóa lồng ghép (deadlock) nếu không cẩn thận trong việc quản lý đối tượng khóa và thứ tự khóa.
Dưới đây là cách sử dụng khối đồng bộ hóa (synchronized block) để đảm bảo tính đồng bộ trong ví dụ trên:
Ví dụ: Counter.java
class Counter {
private int count = 0;
// Đối tượng khóa để đồng bộ hóa
private final Object lock = new Object();
// Phương thức tăng giá trị của biến đếm, sử dụng khối đồng bộ hóa
public void increment() {
synchronized (lock) {
count++;
}
}
// Phương thức lấy giá trị của biến đếm
public int getCount() {
return count;
}
public static void main(String[] args) {
Counter counter = new Counter();
// Tạo hai luồng, mỗi luồng sẽ thực hiện phương thức increment
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
// Bắt đầu hai luồng
t1.start();
t2.start();
// Chờ các luồng kết thúc
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// In ra giá trị cuối cùng của biến đếm
System.out.println("Giá trị cuối cùng: " + counter.getCount());
}
}
Kết quả của chương trình là
Đối tượng lock được sử dụng để đồng bộ hóa. Nó đảm bảo rằng các luồng không thể cùng một lúc thực hiện đoạn mã trong khối đồng bộ hóa. Bạn có thể sử dụng bất kỳ đối tượng nào làm khóa, nhưng thông thường, một đối tượng riêng biệt được sử dụng để đồng bộ hóa.
3. Đồng bộ hóa tĩnh (Static Synchronization)
↳ Khái niệm: Đồng bộ hóa tĩnh tương tự như đồng bộ hóa phương thức, nhưng nó áp dụng cho các phương thức tĩnh. Tất cả các luồng sẽ phải đồng bộ hóa trên cùng một đối tượng khóa tĩnh.
↳ Cách sử dụng: Thêm từ khóa static vào trước phương thức synchronized:
Ví dụ
public static synchronized void staticSynchronizedMethod() {
// mã thực thi an toàn với nhiều luồng
}
↳ Ưu điểm:
↳ Đảm bảo tính nhất quán toàn bộ: Đảm bảo rằng tất cả các luồng phải đồng bộ hóa trên cùng một đối tượng khóa tĩnh, giúp đảm bảo tính nhất quán cho các phương thức tĩnh.
↳ Nhược điểm:
↳ Khóa toàn bộ lớp: Đồng bộ hóa các phương thức tĩnh khóa toàn bộ lớp, có thể dẫn đến tắc nghẽn và giảm hiệu suất nếu nhiều luồng cùng truy cập các phương thức tĩnh đồng bộ hóa.
↳ Khó quản lý: Đối tượng khóa tĩnh là một phần của lớp, không phải của các đối tượng cụ thể, điều này có thể gây khó khăn trong việc quản lý và xác định chính xác khi nào cần đồng bộ hóa.
Tương tự như ví dụ trước, bạn có thể sử dụng đồng bộ hóa tĩnh (Static Synchronization) để đồng bộ hóa một phương thức tĩnh trong lớp. Dưới đây là phiên bản của ví dụ với đồng bộ hóa tĩnh:
Ví dụ: Counter.java
class Counter {
private static int count = 0; // Biến đếm được khai báo là static
// Phương thức tĩnh tăng giá trị của biến đếm, đã đồng bộ hóa
public static synchronized void increment() {
count++;
}
// Phương thức tĩnh lấy giá trị của biến đếm
public static int getCount() {
return count;
}
public static void main(String[] args) {
// Tạo hai luồng, mỗi luồng sẽ thực hiện phương thức increment
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
Counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
Counter.increment();
}
});
// Bắt đầu hai luồng
t1.start();
t2.start();
// Chờ các luồng kết thúc
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// In ra giá trị cuối cùng của biến đếm
System.out.println("Giá trị cuối cùng: " + Counter.getCount());
}
}
Kết quả của chương trình là
Khi sử dụng đồng bộ hóa tĩnh, đồng bộ hóa được thực hiện trên lớp đối tượng chứ không phải trên từng đối tượng riêng lẻ. Điều này có nghĩa là toàn bộ lớp Counter sẽ có một khóa đồng bộ hóa tĩnh chung, và tất cả các luồng phải chờ đợi để có được khóa này khi thực hiện các phương thức tĩnh đồng bộ hóa.