Xin chào mọi người mình lại quay lại với chuỗi bài về concurrency trong java. Bài viết này mình sẽ nói về loại trừ lẫn nhau trong java. Mặc dù mình có nói sẽ không viết về vấn đề này nhưng thôi cứ viết cho đầy đủ vậy.
1. Đặt vấn đề
- Bạn mong đợi chương trình sẽ in ra số bao nhiêu???
public class MutualExclusion implements Runnable {
static int count = 0;
public void add() {
for(int i =0 ; i< 10 ; i++) count ++;
}
@Overridepublic void run() {
add();
}
public static void main(String[] args) throws InterruptedException {
MutualExclusion x = new MutualExclusion();
for (int i = 0; i < 500; i++) {
Thread y = new Thread(x);
y.start();
}
Thread.sleep(1000);
System.out.println(x.count);
}
}
- Nếu bạn nghĩ nó sẽ in ra 5000 thì bài viết này có thể có ích cho bạn.- Sự thật là mình cũng không biết nó sẽ in ra số bao nhiêu?? tùy vào máy của các bạn sẽ nhận được các số khác nhau nhưng có một điều mình chắc chắn là sẽ nhỏ hơn 5000 :))- Bạn hãy thử copy đoạn code trên để chạy thử một vài lần để xem kết quả nhé. Sau đây là một số kết quả từ máy mình: 4990,5000,4997.2. Giải thích- Bất ngờ thật khi mình tìm đáp án cho vấn đề này bằng tiếng việt mình đã không tìm được và mình đã quyết định dùng kiến thức hạn hẹp của mình để giải thích.- Trước tiên ta phải hiểu được một phép tính cộng trong ngôn ngữ lập trình sẽ như thế nào. Khi biên dịch chương trình biến count sẽ được lưu vào một ô nhớ trong ram. khi một luồng hay một đoạn code muốn tăng giá trị của biến count nó sẽ phải làm các việc sau đây:a. Đọc giá trị của biến count trong ramb. Tăng giá trị biến countc. Lưu giá trị biến count ngược lại vào ram vào đúng địa chỉ mà nó đọc ra
- Mong bạn sẽ hiểu đoạn giải thích trên của mình. Vậy quay lại với chương trình và đặt câu hỏi tại sao chương trình kia lại không đưa ra kết quả là 5000. Như bạn thấy đó trong chương trình trên ta có tạo ra 500 luồng chạy đồng thời nghĩa là các luồng sẽ không chờ nhau kết thúc. Như vậy sẽ có thể sảy ra trường hợp sau đây ta có 2 luồng cùng đọc một giá trị của biến count từ ram ra để thực hiện việc cộng ( giả sử count = 10) sau đó 2 luồng thực hiện tăng giá trị biến count lên và ghi giá trị đó ngược lại vào ram. Vậy sau khi 2 luồng kết thúc biến count không thể tăng lên 20 giá trị như những dòng code của chúng ta. Hy vọng các bạn hiểu được ý của mình. Còn nếu các bạn không hiểu thì kệ các bạn chỉ cần nhớ nếu code đa luồng thế này sẽ rất nguy hiểm mà thôi.
3. Cách giải quyết vấn đề
- Vấn đề bên trên được gọi là xử lý đoạn găng trong lập trình phân tán, song song hay đa luồng. Biến count là tài nguyên găng trong trường hợp này vì biến count được nhiều luồng truy cập tới.
- Cách giải quyết đơn giản là chỉ cho phép một luồng duy nhất được thao tác với tài nguyên găng trong đoạn găng.
- Sử dụng từ khóa synchronized trong java. Từ khóa synchronized cho phép một luồng duy nhất được thao tác với tài nguyên găng ( count ).
- Ta sẽ thay đổi code như sau đây. Mình sẽ đưa ra2 cách sử dụng từ khóa synchronized.
- Cách 1:
public class MutualExclusion implements Runnable {
static int count = 0;
public synchronized void add() {
for(int i =0 ; i< 10 ; i++) count ++;
}
@Overridepublic void run() {
add();
}
public static void main(String[] args) throws InterruptedException {
MutualExclusion x = new MutualExclusion();
for (int i = 0; i < 500; i++) {
Thread y = new Thread(x);
y.start();
}
Thread.sleep(1000);
System.out.println(x.count);
}
}
- Khi ta đặt từ khóa synchronized trước một hàm hay phương thức thì tại một thời điểm chỉ có 1 luồng được thao tác với hàm hay phương thức đó. Cách này dễ dùng nhưng có nhược điểm nếu hàm hay phương thức đó có nhiều logic phức tạp nhưng không liên quan đến đoạn găng thì sẽ gây tốn thời gian. Vậy lời khuyên của mình là hãy dùng cách này cho các hàm đơn giản hoặc hãy tách đoạn xử lý với đoạn găng thành một hàm riêng nhé.- Cách 2 :
public class MutualExclusion implements Runnable {
static int count = 0;
public void add() {
for (int i = 0; i < 10; i++) {
synchronized (this){
count++;
}
}
}
@Overridepublic void run() {
add();
}
public static void main(String[] args) throws InterruptedException {
MutualExclusion x = new MutualExclusion();
for (int i = 0; i < 500; i++) {
Thread y = new Thread(x);
y.start();
}
Thread.sleep(1000);
System.out.println(x.count);
}
}
- Cách này ta có thể khóa một đoạn code trong một hàm của bạn chỉ cho một luồng thao tác với tài nguyên găng. Ưu nhược điểm cách này ngược lại với cách kia nhé.
4. Lời kết
- Hy vọng bài viết có ích cho các bạn. Cảm ơn các bạn đã đọc đến đây. Mình sẽ còn ra thêm bài viết về vấn đề Concurrency in Java xin chào và không hẹn gặp lại.