Trong lập trình đa luồng, synchronized là một trong những công cụ quan trọng để điều phối truy cập đến các tài nguyên chia sẻ, nhằm đảm bảo rằng không có hai luồng nào có thể truy cập cùng một tài nguyên tại cùng một thời điểm. Từ khóa synchronized cung cấp một cơ chế để đồng bộ hóa các luồng trong Java, giúp tránh các vấn đề như điều kiện đua (race conditions) và bảo đảm tính nhất quán của dữ liệu. Bài viết này sẽ trình bày chi tiết về synchronized, cách sử dụng, cùng với ví dụ minh họa và một số lưu ý khi làm việc với nó.

1. Khái Niệm

Từ khóa synchronized trong Java được sử dụng để chỉ định rằng một phương thức hoặc một khối mã chỉ có thể được thực hiện bởi một luồng tại một thời điểm nhất định. Khi một luồng gọi một phương thức hoặc khối mã được đánh dấu là synchronized, nó sẽ chiếm quyền sở hữu của một khóa (lock). Khi đó, các luồng khác không thể truy cập vào phương thức hoặc khối mã đó cho đến khi luồng đang sở hữu khóa hoàn tất.

2. Cách Hoạt Động

Khi một luồng gọi một phương thức hoặc khối mã được đánh dấu là synchronized, nó sẽ làm như sau:

  • Chiếm quyền sở hữu khóa: Luồng sẽ chiếm quyền sở hữu khóa của đối tượng hoặc lớp mà phương thức đó thuộc về.
  • Ngăn chặn truy cập đồng thời: Các luồng khác sẽ bị chặn khi cố gắng truy cập vào phương thức hoặc khối mã đó cho đến khi luồng đang sở hữu khóa hoàn tất và giải phóng khóa.
  • Giải phóng khóa: Khi luồng kết thúc thực hiện phương thức hoặc khối mã, khóa sẽ được giải phóng, cho phép các luồng khác có thể truy cập vào.

3. Cách Sử Dụng Synchronized

3.1. Synchronized Method

Phương thức được khai báo là synchronized sẽ chiếm khóa của đối tượng mà nó thuộc về:

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

Trong ví dụ trên, phương thức increment()getCount() được khai báo là synchronized, đảm bảo rằng chỉ có một luồng có thể thực hiện chúng tại một thời điểm.

3.2. Synchronized Block

Đối với trường hợp bạn chỉ cần đồng bộ hóa một phần của mã, bạn có thể sử dụng synchronized block:

public class Counter {
    private int count = 0;

    public void increment() {
        synchronized (this) {
            count++;
        }
    }

    public int getCount() {
        synchronized (this) {
            return count;
        }
    }
}

Ở đây, chỉ có phần mã bên trong khối synchronized mới được đồng bộ hóa, cho phép tăng tính linh hoạt và giảm thiểu độ trễ.

3.3. Synchronized Class

Bạn cũng có thể đồng bộ hóa một phương thức tĩnh, trong trường hợp này, khóa sẽ thuộc về lớp thay vì một thể hiện cụ thể:

public class Counter {
    private static int count = 0;

    public static synchronized void increment() {
        count++;
    }

    public static synchronized int getCount() {
        return count;
    }
}

4. Ví Dụ Minh Họa

Dưới đây là một ví dụ hoàn chỉnh về việc sử dụng synchronized trong một ứng dụng đa luồng:

public class SynchronizedExample {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }

    public static void main(String[] args) {
        SynchronizedExample example = new SynchronizedExample();

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final count: " + example.getCount());
    }
}

Trong ví dụ này, hai luồng cùng nhau gọi phương thức increment() trên cùng một đối tượng SynchronizedExample. Do phương thức được đánh dấu là synchronized, giá trị của biến count sẽ được cập nhật một cách an toàn mà không xảy ra tình trạng đua.

5. Lưu Ý Khi Sử Dụng

  • Khóa lâu: Tránh giữ khóa quá lâu vì nó có thể làm chậm hiệu suất của ứng dụng và có thể dẫn đến deadlock.
  • Nên sử dụng synchronized block: Nếu chỉ cần đồng bộ hóa một phần nhỏ của mã, sử dụng khối synchronized sẽ giúp tăng hiệu suất.
  • Tạo khóa không đồng bộ: Cẩn thận với việc tạo khóa không đồng bộ (ví dụ: khóa trên một đối tượng không nên được chia sẻ giữa các luồng).

6. Kết Luận

Từ khóa synchronized trong Java là một công cụ quan trọng để đảm bảo tính đồng bộ giữa các luồng khi truy cập tài nguyên chia sẻ. Nó giúp tránh tình trạng đua và đảm bảo rằng dữ liệu luôn nhất quán. Tuy nhiên, việc sử dụng synchronized cần phải được thực hiện cẩn thận để tránh ảnh hưởng tiêu cực đến hiệu suất và khả năng mở rộng của ứng dụng.