Tiếng Việt - Java - Đồng bộ hóa

Xin chào các bạn, những nhà phép thuật Java tương lai! Hôm nay, chúng ta sẽ khám phá một trong những khái niệm quan trọng nhất trong lập trình Java: Đồng bộ hóa. Đừng lo lắng nếu bạn mới bắt đầu học lập trình; tôi sẽ hướng dẫn bạn qua cuộc hành trình này bước từng bước, như thế mà tôi đã làm cho nhiều học viên khác trong những năm dạy học. Vậy hãy lấy ly cà phê yêu thích của bạn, thư giãn và hãy cùng nhau bắt đầu cuộc phiêu lưu thú vị này!

Java - Synchronization

Đồng bộ hóa là gì và Tại sao Chúng ta Cần Nó?

Hãy tưởng tượng bạn đang ở trong một bếp bếp buồng với nhiều đầu bếp cố gắng chuẩn bị một món ăn phức tạp. Nếu họ tất cả đều đặt tay vào nguyên liệu mà không có sự phối hợp, sự hỗn loạn sẽ xảy ra! Đó chính là điều gì có thể xảy ra trong một chương trình Java khi nhiều luồng cố gắng truy cập tài nguyên chia sẻ đồng thời. Đây là nơi mà đồng bộ hóa xuất hiện để cứu rỗi!

Đồng bộ hóa trong Java như có một cảnh sát giao thông trong bếp bếp buồng đó, đảm bảo rằng chỉ một đầu bếp (luồng) có thể sử dụng một nguyên liệu cụ thể (tài nguyên) tại một thời điểm. Nó giúp duy trì thứ tự và ngăn chặn xung đột, đảm bảo rằng chương trình của chúng ta chạy suôn sẻ mà không có sự bất ngờ không mong muốn.

Nhu cầu đồng bộ hóa luồng

Hãy xem một ví dụ thực tế để hiểu tại sao đồng bộ hóa lại quan trọng:

public class BankAccount {
    private int balance = 1000;

    public void withdraw(int amount) {
        if (balance >= amount) {
            System.out.println(Thread.currentThread().getName() + " is about to withdraw...");
            balance -= amount;
            System.out.println(Thread.currentThread().getName() + " has withdrawn " + amount);
        } else {
            System.out.println("Sorry, not enough balance for " + Thread.currentThread().getName());
        }
    }

    public int getBalance() {
        return balance;
    }
}

Trong ví dụ này, chúng ta có một lớp BankAccount với phương thức withdraw đơn giản. Có vẻ dễ hiểu phải không? Nhưng điều gì sẽ xảy ra khi hai người cùng lúc cố gắng rút tiền? Hãy tìm hiểu thêm!

public class UnsynchronizedBankDemo {
    public static void main(String[] args) {
        BankAccount account = new BankAccount();

        Thread john = new Thread(() -> {
            account.withdraw(800);
        }, "John");

        Thread jane = new Thread(() -> {
            account.withdraw(800);
        }, "Jane");

        john.start();
        jane.start();

        try {
            john.join();
            jane.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final Balance: " + account.getBalance());
    }
}

Khi bạn chạy chương trình này, bạn có thể thấy cái nhìn như thế này:

John is about to withdraw...
Jane is about to withdraw...
John has withdrawn 800
Jane has withdrawn 800
Final Balance: -600

Chờ, sao lại vậy? Tại sao chúng ta lại có số dư âm? Đây, các bạn ơi, chúng ta gọi nó là tình huống cạnh tranh. Cả John và Jane đã kiểm tra số dư, thấy có đủ tiền và rút. Đây chính là lý do tại sao chúng ta cần đồng bộ hóa!

Thực hiện Đồng bộ hóa trong Java

Bây giờ khi chúng ta đã thấy vấn đề, hãy xem cách Java giúp chúng ta giải quyết nó. Java cung cấp nhiều cách để thực hiện đồng bộ hóa, nhưng chúng ta sẽ tập trung vào hai cách phổ biến nhất:

  1. Phương thức đồng bộ hóa
  2. Khối đồng bộ hóa

Phương thức đồng bộ hóa

Cách dễ nhất để thêm đồng bộ hóa là sử dụng từ khóa synchronized trong phát động phương thức. Hãy sửa đổi lớp BankAccount của chúng ta:

public class BankAccount {
    private int balance = 1000;

    public synchronized void withdraw(int amount) {
        if (balance >= amount) {
            System.out.println(Thread.currentThread().getName() + " is about to withdraw...");
            balance -= amount;
            System.out.println(Thread.currentThread().getName() + " has withdrawn " + amount);
        } else {
            System.out.println("Sorry, not enough balance for " + Thread.currentThread().getName());
        }
    }

    public int getBalance() {
        return balance;
    }
}

Bây giờ, khi chúng ta chạy chương trình với phương thức đồng bộ hóa này, chúng ta sẽ nhận được kết quả rất hợp lý hơn:

John is about to withdraw...
John has withdrawn 800
Sorry, not enough balance for Jane
Final Balance: 200

Rất tốt! Chỉ một luồng có thể thực hiện phương thức withdraw tại một thời điểm, ngăn chặn vấn đề của chúng ta trước đó.

Khối đồng bộ hóa

Đôi khi, bạn có thể không muốn đồng bộ hóa toàn bộ phương thức. Trong những trường hợp này, bạn có thể sử dụng khối đồng bộ hóa. Đây là cách:

public class BankAccount {
    private int balance = 1000;
    private Object lock = new Object(); // Đây là đối tượng khóa của chúng ta

    public void withdraw(int amount) {
        synchronized(lock) {
            if (balance >= amount) {
                System.out.println(Thread.currentThread().getName() + " is about to withdraw...");
                balance -= amount;
                System.out.println(Thread.currentThread().getName() + " has withdrawn " + amount);
            } else {
                System.out.println("Sorry, not enough balance for " + Thread.currentThread().getName());
            }
        }
    }

    public int getBalance() {
        return balance;
    }
}

Điều này đạt được cùng kết quả như phương thức đồng bộ hóa, nhưng cung cấp cho chúng ta kiểm soát tốt hơn về phần mã nào cần được đồng bộ hóa.

Tầm quan trọng của Đồng bộ hóa Hợp lý

Bây giờ, bạn có thể suy nghĩ, "Tuyệt vời! Tôi sẽ chỉ đồng bộ hóa mọi thứ!" Nhưng đừng vội vã! Đồng bộ hóa quá nhiều có thể dẫn đến vấn đề hiệu suất. Nó như để đặt một đèn giao thông tại mỗi giao điểm trên một thị trấn nhỏ - nó có thể an toàn, nhưng sẽ làm chậm mọi thứ xuống mức chậm lắm.

Chìa khóa là đồng bộ hóa chỉ những thứ cần thiết. Trong ví dụ tài khoản ngân hàng của chúng ta, chúng ta chỉ cần đồng bộ hóa phần kiểm tra và cập nhật số dư.

Các phương thức Đồng bộ hóa phổ biến

Java cung cấp nhiều phương thức hữu ích để làm việc với đồng bộ hóa. Dưới đây là bảng một số phương thức phổ biến:

Phương thức Mô tả
wait() Gây ra luồng hiện tại chờ đợi cho đến khi một luồng khác gọi notify() hoặc notifyAll()
notify() Thức dậy một luồng đang chờ đợi trên đối tượng giám sát này
notifyAll() Thức dậy tất cả các luồng đang chờ đợi trên đối tượng giám sát này
join() Chờ đợi một luồng chết

Những phương thức này có thể rất hữu ích khi bạn cần các kịch bản đồng bộ hóa phức tạp hơn.

Kết luận

Và thế là, các bạn ơi! Chúng ta đã duyệt qua thế giới của đồng bộ hóa Java, từ việc hiểu tại sao chúng ta cần nó đến việc triển khai trong mã của chúng ta. Hãy nhớ, đồng bộ hóa như muối trong nấu ăn – sử dụng đủ để làm tươi mã của bạn, nhưng không nên quá nhiều để làm mạnh mẽ mọi thứ khác.

Khi bạn tiếp tục hành trình Java của mình, bạn sẽ gặp phải nhiều kịch bản đồng bộ hóa phức tạp hơn. Nhưng đừng lo lắng! Với nền tảng này, bạn đã sẵn sàng để đối mặt với bất kỳ thử thách đồng luồng nào.

Hãy tiếp tục lập trình, tiếp tục học hỏi và quan trọng nhất, hãy có niềm vui!毕竟, lập trình cũng là một nghệ thuật như là một khoa học. Chờ đợi lần gặp lại, may các luồng của bạn được đồng bộ hóa và Java của bạn mạnh mẽ!

Credits: Image by storyset