Vấn Đề Đa Luồng
(Multithreading Issues)

Các luồng (threads) trong Java giao tiếp chủ yếu bằng cách chia sẻ quyền truy cập vào các trường (fields) và các đối tượng mà các trường tham chiếu tới. Dạng giao tiếp này rất hiệu quả, nhưng cũng dẫn đến hai loại lỗi tiềm ẩn: xung đột luồng (thread interference) và lỗi nhất quán bộ nhớ (memory consistency errors). Công cụ cần thiết để ngăn chặn những lỗi này là đồng bộ hóa.

Tuy nhiên, đồng bộ hóa có thể gây ra cạnh tranh luồng (thread contention), xảy ra khi hai hoặc nhiều luồng cố gắng truy cập cùng một tài nguyên đồng thời, khiến Java runtime phải thực thi một hoặc nhiều luồng chậm hơn, hoặc thậm chí tạm dừng thực thi của chúng. Starvation và livelock là những dạng cạnh tranh luồng. Dưới đây là một số vấn đề phổ biến đã được đề cập ở trên:

1. Xung đột luồng (Thread Interference)

Khi nhiều luồng cùng truy cập và thao tác trên cùng một đối tượng mà không được đồng bộ hóa, có thể xảy ra xung đột luồng (thread interference). Xung đột luồng xảy ra khi các thao tác, thực hiện trên cùng một dữ liệu nhưng từ các luồng khác nhau, chồng chéo lên nhau, dẫn đến việc dữ liệu không được cập nhật như mong đợi.

Ví dụ: Nếu hai luồng cùng cố gắng cập nhật một biến đồng thời mà không được đồng bộ hóa, kết quả có thể không như mong đợi, vì mỗi luồng có thể ghi đè lên thay đổi của luồng khác.

Ví dụ: Example.java

class Example {
    private int counter = 0;

    public void TangGiaTri() {
        counter++;
    }

    public void GiamGiaTri() {
        counter--;
    }

    public int GiaTri() {
        return counter;
    }
}

Giả sử Luồng A gọi TangGiaTri() vào khoảng thời gian Luồng B gọi GiamGiaTri(). Nếu giá trị ban đầu của count là 0, các hành động chồng chéo của chúng có thể theo trình tự sau:

↳ Luồng A: Lấy giá trị của count (giá trị là 0).

↳ Luồng B: Lấy giá trị của count (giá trị vẫn là 0).

↳ Luồng A: Tăng giá trị đã lấy lên 1 (kết quả là 1).

↳ Luồng B: Giảm giá trị đã lấy xuống -1 (kết quả là -1).

↳ Luồng A: Lưu kết quả vào count (giá trị của c là 1).

↳ Luồng B: Lưu kết quả vào count (giá trị của c là -1).

Kết quả của Luồng A bị mất, bị ghi đè bởi kết quả của Luồng B. Đây chỉ là một ví dụ về sự chồng chéo có thể xảy ra. Trong các tình huống khác, có thể là kết quả của Luồng B bị mất hoặc không có lỗi nào xảy ra. Vì chúng không thể đoán trước, các lỗi do xung đột luồng có thể khó phát hiện và sửa chữa.

Cách giải quyết

Để giải quyết vấn đề xung đột luồng, bạn cần phải đồng bộ hóa các thao tác thay đổi dữ liệu. Điều này có thể được thực hiện bằng cách sử dụng từ khóa synchronized trong Java để đảm bảo rằng chỉ một luồng có thể truy cập hoặc thay đổi dữ liệu tại một thời điểm.

2. Lỗi nhất quán bộ nhớ (Memory Consistency Errors):

Lỗi nhất quán bộ nhớ xảy ra khi các luồng khác nhau không nhìn thấy cùng một giá trị của dữ liệu, dù dữ liệu đó nên giống nhau. Ví dụ, một luồng có thể thay đổi giá trị của biến, nhưng luồng khác có thể không thấy sự thay đổi đó ngay lập tức.

Khái niệm Happens-Before

Chìa khóa để tránh lỗi nhất quán bộ nhớ (Memory Consistency Errors) là hiểu mối quan hệ happens-before. Mối quan hệ này đảm bảo rằng các thao tác ghi bộ nhớ của một câu lệnh cụ thể sẽ được nhìn thấy bởi một câu lệnh khác cụ thể. Để hiểu điều này, hãy xem xét ví dụ sau:

Giả sử có một biến int được định nghĩa và khởi tạo như sau:

Ví dụ

int counter = 0;

Biến counter được chia sẻ giữa hai luồng, A và B. Giả sử luồng A tăng giá trị counter:

Ví dụ

counter++;

Ngay sau đó, luồng B in giá trị counter:

Ví dụ

System.out.println(counter);

Nếu hai câu lệnh này được thực hiện trong cùng một luồng, bạn có thể yên tâm rằng giá trị được in ra sẽ là "1". Nhưng nếu hai câu lệnh được thực hiện trong các luồng khác nhau, giá trị được in ra có thể là "0" vì không có đảm bảo rằng thay đổi của luồng A với counter sẽ được nhìn thấy bởi luồng B — trừ khi lập trình viên đã thiết lập một mối quan hệ happens-before giữa hai câu lệnh này.

Các hành động tạo mối quan hệ Happens-Before

Có một số hành động tạo ra mối quan hệ happens-before. Hai hành động đã được đề cập là:

↳ Khi một câu lệnh gọi Thread.start: Mỗi câu lệnh có mối quan hệ happens-before với câu lệnh đó cũng có mối quan hệ happens-before với mọi câu lệnh được thực hiện bởi luồng mới. Các hiệu ứng của mã trước khi tạo luồng mới sẽ được nhìn thấy bởi luồng mới.

↳ Khi một luồng kết thúc và gây ra Thread.join: Khi Thread.join trong một luồng khác trả về, tất cả các câu lệnh thực hiện bởi luồng kết thúc đều có mối quan hệ happens-before với tất cả các câu lệnh sau khi join thành công. Các hiệu ứng của mã trong luồng hiện đã được nhìn thấy bởi luồng thực hiện join.

Ví dụ cụ thể

Dưới đây là một ví dụ đơn giản hơn, làm rõ hơn về cách mối quan hệ happens-before hzạt động:

Ví dụ

public class Example {
    private int counter = 0;

    // Phương thức để tăng giá trị của counter
    public synchronized void increment() {
        counter++;
    }

    // Phương thức để lấy giá trị của counter
    public int getCounter() {
        return counter;
    }

    public static void main(String[] args) throws InterruptedException {
        Example example = new Example();

        // Tạo luồng để thực hiện tăng giá trị counter
        Thread threadTangGiaTri = new Thread(() -> {
            example.increment();
        });

        // Tạo luồng để in giá trị counter
        Thread threadInGiaTri = new Thread(() -> {
            System.out.println(example.getCounter());
        });

        // Bắt đầu luồng tăng giá trị counter
        threadTangGiaTri.start();

        // Đợi luồng tăng giá trị counter hoàn thành
        threadTangGiaTri.join();

        // Bắt đầu luồng in giá trị counter
        threadInGiaTri.start();

        // Đợi luồng in giá trị counter hoàn thành
        threadInGiaTri.join();
    }
}

Kết luận

↳ Synchronized Method: TangGiaTri() sử dụng đồng bộ hóa để đảm bảo tính nhất quán khi thay đổi giá trị của counter.

↳ Join: threadTangGiaTri.join() đảm bảo rằng threadInGiaTri sẽ không bắt đầu cho đến khi threadTangGiaTri hoàn tất. Điều này giúp thiết lập mối quan hệ happens-before giữa hai luồng.

↳ Nếu không có join() hoặc đồng bộ hóa, bạn có thể gặp tình trạng lỗi không nhất quán bộ nhớ (memory consistency errors) vì không có đảm bảo rằng các thay đổi trong một luồng sẽ được nhìn thấy bởi các luồng khác.

3. Cạnh tranh luồng (Thread Contention):

Cạnh tranh luồng xảy ra khi hai hoặc nhiều luồng cố gắng truy cập cùng một tài nguyên đồng thời. Điều này có thể khiến Java runtime phải thực thi một hoặc nhiều luồng chậm hơn hoặc thậm chí tạm dừng thực thi của chúng. Starvation và livelock là những dạng cạnh tranh luồng. Hãy cùng diễn giải hai khái niệm Starvation và Livelock một cách dễ hiểu hơn.

Starvation (Thiếu tài nguyên)

Starvation xảy ra khi một luồng (thread) không thể tiếp cận được tài nguyên cần thiết để hoàn thành công việc của nó. Điều này thường xảy ra khi có những luồng khác liên tục chiếm lấy tài nguyên đó, khiến luồng đầu tiên phải chờ đợi quá lâu và không thể thực hiện công việc của mình.

↳ Tình huống minh họa: Hãy tưởng tượng bạn đang xếp hàng mua vé xem phim, nhưng có một số người cứ chen ngang vào trước bạn. Bạn phải chờ mãi mà không bao giờ đến lượt mình. Đây chính là Starvation — bạn không thể mua được vé vì những người khác luôn lấy đi cơ hội của bạn.

Livelock (Khóa sống)

Livelock xảy ra khi các luồng liên tục thay đổi trạng thái để cố gắng tránh va chạm hoặc giải quyết một vấn đề, nhưng kết quả là chúng không thể tiến triển được công việc thực sự. Các luồng không bị chặn lại, nhưng chúng cứ mãi "nhường đường" cho nhau mà không thể làm gì khác.

↳ Tình huống minh họa: Hãy tưởng tượng bạn và một người bạn đang đi trong một hành lang hẹp. Bạn bước sang trái để nhường đường, người bạn cũng bước sang phải để nhường đường. Thấy rằng vẫn đang chắn đường nhau, bạn lại bước sang phải, còn người bạn lại bước sang trái. Cả hai người cứ thế mãi mà không ai qua được. Đây chính là Livelock — cả hai đều muốn giải quyết vấn đề, nhưng lại bị mắc kẹt trong việc nhường nhịn nhau.

Câu Nói Truyền Cảm Hứng

“Bắt đầu ở đâu không quan trọng, quan trọng là bạn sẵn sàng bắt đầu.” – W. Clement Stone

Không Gian Tích Cực

“Chúc bạn luôn giữ vững niềm tin và sức mạnh để vượt qua mọi thử thách trong cuộc sống.”