Khóa Nội Bộ Trong Lập Trình Đồng Bộ Java

Khóa Nội Bộ Trong Lập Trình Đồng Bộ Java

Khi làm việc với lập trình đa luồng, việc đảm bảo an toàn luồng (thread safety) là một khía cạnh quan trọng. Trong Java, cơ chế khóa nội bộ (built-in locks) đóng vai trò then chốt trong việc thực hiện đồng bộ hóa. Bài viết này sẽ đi sâu vào hiểu biết về khóa nội bộ và cách sử dụng chúng để đảm bảo an toàn luồng trong các ứng dụng Java.

Từ Mẫu Đơn Thế Giới Nhập Môn

Đầu tiên, chúng ta hãy xem xét vấn đề an toàn luồng qua ví dụ về mẫu thiết kế Singleton. Singleton là một mẫu thiết kế đảm bảo rằng một lớp chỉ có một thể hiện duy nhất và cung cấp một điểm truy cập toàn cục đến nó.

Phương thức khởi tạo lười (lazy initialization) của Singleton như sau:

public class Singleton {
    private Singleton() {
    }

    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

Phương thức này hoạt động tốt trong môi trường đơn luồng, nhưng trong môi trường đa luồng, có thể xảy ra tình trạng nhiều luồng cùng kiểm tra điều kiện `instance == null` và cùng tạo thể hiện mới, vi phạm nguyên tắc Singleton. Để giải quyết vấn đề này, chúng ta có thể sử dụng từ khóa `synchronized`:

public class Singleton {
    private Singleton() {
    }

    private static Singleton instance;

    public synchronized static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

Tuy nhiên, cách triển khai này có hiệu suất thấp vì phương thức bị khóa ngay cả sau khi đối tượng đã được tạo. Một giải pháp cải tiến hơn là sử dụng Double-Checked Locking (DCL):

public class Singleton {
    private Singleton() {
    }

    private static volatile Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Từ khóa `volatile` được sử dụng để đảm bảo rằng biến `instance` được ghi vào bộ nhớ chính ngay lập tức và được đọc từ bộ nhớ chính mỗi khi truy cập, tránh các vấn đề do tối ưu hóa của bộ nhớ đệm CPU.

Đồng Bộ Hóa và Từ Khóa Synchronized

Khóa Khối Đồng Bộ

Java cung cấp cơ chế khóa nội bộ để thực hiện khối đồng bộ. Khối đồng bộ bao gồm hai phần: một tham chiếu đối tượng làm khóa và một khối mã được bảo vệ bởi khóa:

synchronized (lockObject) {
    // Khối mã được bảo vệ bởi khóa
}

Mỗi đối tượng Java có thể được sử dụng làm khóa để thực hiện đồng bộ. Khóa nội bộ của Java còn được gọi là khóa độc quyền (mutex), nghĩa là chỉ có một luồng có thể giữ khóa tại một thời điểm. Khi một luồng giữ khóa, các luồng khác phải đợi cho đến khi khóa được giải phóng.

Từ Khóa Synchronized Áp Dụng Cho Phương Thức

Phương thức được đánh dấu bằng từ khóa `synchronized` hoạt động như một khối đồng bộ trên toàn bộ phương thức, với khóa là đối tượng gọi phương thức:

class Example {
    public synchronized void doSomething() {
        // Mã phương thức
    }
}

Điều này tương đương với:

class Example {
    public void doSomething() {
        synchronized(this) {
            // Mã phương thức
        }
    }
}

Đối với phương thức tĩnh, khóa là đối tượng `Class` của lớp:

class Example {
    public static synchronized void doSomething() {
        // Mã phương thức
    }
}

Tương đương với:

class Example {
    public static void doSomething() {
        synchronized(Example.class) {
            // Mã phương thức
        }
    }
}

Khóa Có Thể Nhập Lại (Reentrant Lock)

Khóa nội bộ của Java là khóa có thể nhập lại (reentrant), nghĩa là một luồng có thể nhận được một khóa mà nó đã giữ. Điều này ngăn chặn tình trạng deadlock khi một luồng cố gắng gọi một phương thức được đồng bộ hóa từ bên trong một phương thức khác được đồng bộ hóa trên cùng một đối tượng:

public class ReentrantExample {
    public synchronized void methodA() {
        System.out.println("Phương thức A");
        methodB();
    }

    public synchronized void methodB() {
        System.out.println("Phương thức B");
    }
}

Khi một luồng gọi `methodA`, nó nhận được khóa đối tượng. Khi `methodA` gọi `methodB`, luồng đó có thể nhập lại vào cùng một khóa mà không cần đợi.

Từ Khóa Volatile

Tính Nguyên Tử

Nguyên tử là các thao tác không thể chia nhỏ. Trong Java, một số thao tác như `x = 5` là nguyên tử, nhưng các thao tác như `x++` không phải là nguyên tử vì chúng bao gồm các bước "đọc-đổi mới-ghi".

Để đảm bảo tính nguyên tử cho các thao tác phức tạp, Java cung cấp các lớp trong gói `java.util.concurrent.atomic`, như `AtomicInteger`, `AtomicLong`, v.v.

Tính Khả Thấy (Visibility)

Khả thấy đề cập đến việc liệu các thay đổi trạng thái của một luồng có được nhìn thấy bởi các luồng khác hay không. Khi một luồng sửa đổi một biến, các luồng khác không nhất thiết sẽ thấy thay đổi ngay lập tức.

Biến được đánh dấu bằng `volatile` đảm bảo khả thấy:

public class VolatileExample {
    private volatile boolean flag = false;

    public void run() {
        new Thread(() -> {
            while (!flag) {
                // Vòng lặp chờ
            }
            System.out.println("Kết thúc vòng lặp");
        }).start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        flag = true;
    }
}

Từ khóa `volatile` đảm bảo rằng giá trị của `flag` luôn được đọc từ bộ nhớ chính và được ghi vào bộ nhớ chính ngay lập tức.

Cấm Tái Sắp Xếp (Reordering)

Biến `volatile` cũng ngăn chặn việc tái sắp xếp các thao tác với biến này bởi trình biên dịch hoặc JVM, đảm bảo rằng các thao tác được thực hiện theo thứ tự được chỉ định trong mã nguồn.

Synchronized và Khả Thấy

Mặc dù `synchronized` chủ yếu được sử dụng để kiểm soát truy cập vào các khối mã được đồng bộ hóa, nó cũng đảm bảo khả thấy. Khi một luồng thoát khỏi một khối mã được đồng bộ hóa, tất cả các thay đổi đối với biến mà nó thực hiện sẽ được đồng bộ hóa với bộ nhớ chính, và khi một luồng vào một khối mã được đồng bộ hóa, nó sẽ đọc các giá trị mới nhất từ bộ nhớ chính.

Tuy nhiên, `synchronized` và `volatile` phục vụ các mục đích khác nhau. `synchronized` cung cấp cơ chế khóa để kiểm soát truy cập vào các khối mã, trong khi `volatile` chỉ đảm bảo khả thấy và ngăn chặn tái sắp xếp cho biến riêng lẻ.

Thẻ: khóa nội bộ đồng bộ hóa Java volatile Thread Safety Singleton Pattern

Đăng vào ngày 22 tháng 5 lúc 16:06