So sánh CAS và AQS trong lập trình đồng bộ Java

Khía cạnh CAS AQS
Vai trò Hướng dẫn nguyên tử của CPU / nguyên thủy đồng bộ cấp thấp Khung cấp cao do Java cung cấp để xây dựng các cơ chế đồng bộ
Tư tưởng chính Khóa lạc quan, không khóa, thực hiện so sánh rồi hoán đổi Mẫu thiết kế phương thức khuôn mẫu, kết hợp hàng đợi và quản lý trạng thái
Cấp độ triển khai Hướng dẫn phần cứng (mức assembly) Thư viện Java (dựa trên CAS và cấu trúc hàng đợi)
Trọng tâm Đảm bảo tính nguyên tử cho một biến chia sẻ đơn lẻ Quản lý thứ tự truy cập tài nguyên chia sẻ giữa nhiều luồng
Bài toán giải quyết Thao tác nguyên tử đơn giản, mức độ chi tiết thấp (ví dụ: i++) Điều khiển đồng bộ phức tạp, mức độ cao (ví dụ: khóa, semaphore)
Đặc điểm hiệu năng Hiệu suất rất cao khi xung đột ít; tiêu tốn CPU nếu xung đột nhiều do vòng lặp tự quay Khi xung đột cao, luồng bị đưa vào hàng đợi và treo, tránh lãng phí CPU
Mối quan hệ Là nền tảng để AQS hoạt động — AQS dùng CAS để thao tác state và nút hàng đợi Là người dùng cấp cao của CAS — tích hợp CAS, hàng đợi và điều phối luồng

AQS (AbstractQueuedSynchronizer)

1. AQS là gì?

AQS là lớp trừu tượng nằm trong gói java.util.concurrent, đóng vai trò khung nền để xây dựng các cơ chế đồng bộ như khóa hoặc rào cản. Nó áp dụng mẫu thiết kế Template Method.

2. Nguyên lý hoạt động

AQS duy trì:

  • Một biến volatile int state biểu diễn trạng thái đồng bộ.
  • Một hàng đợi hai chiều FIFO (biến thể của CLH queue) để quản lý các luồng đang chờ.

Ý nghĩa của state tùy theo ngữ cảnh:

  • ReentrantLock: số lần tái nhập của luồng giữ khóa.
  • Semaphore: số lượng giấy phép còn lại.
  • CountDownLatch: giá trị đếm ngược còn lại.

Khi một luồng không lấy được tài nguyên, nó được đóng gói thành Node và thêm vào cuối hàng đợi bằng thao tác CAS. Khi tài nguyên được giải phóng, luồng đầu hàng đợi sẽ được đánh thức để thử lại.

3. Ứng dụng thực tế trong Java

  • ReentrantLock
  • Semaphore
  • CountDownLatch
  • ReentrantReadWriteLock
  • ThreadPoolExecutor.Worker

Các lớp này đều chứa lớp con nội bộ kế thừa từ AQS và ghi đè các phương thức như tryAcquire(), tryRelease() để định nghĩa logic đồng bộ riêng.

4. Cơ chế hoạt động (minh họa qua ReentrantLock)

  1. Luồng gọi lock().
  2. Lớp con của AQS dùng CAS để đổi state từ 0 → 1. Nếu thành công, ghi nhận luồng sở hữu.
  3. Nếu thất bại (do state ≠ 0), luồng được đưa vào hàng đợi và treo.
  4. Khi gọi unlock(), state được đặt lại, và luồng tiếp theo trong hàng đợi được đánh thức.

Toàn bộ quá trình quản lý trạng thái và hàng đợi đều dựa vào CAS để đảm bảo an toàn đa luồng.

CAS (Compare-And-Swap)

1. CAS là gì?

CAS là hướng dẫn nguyên tử của CPU, được Java phơi bày qua lớp sun.misc.Unsafe hoặc các lớp trong gói java.util.concurrent.atomic.

2. Nguyên lý

CAS gồm ba tham số: địa chỉ bộ nhớ (V), giá trị kỳ vọng (A), giá trị mới (B):

  • Nếu giá trị tại V bằng A, ghi B vào V và trả về true.
  • Ngược lại, trả về false (không thay đổi).
boolean compareAndSwap(int* V, int A, int B) {
    if (*V == A) {
        *V = B;
        return true;
    }
    return false;
}

3. Ứng dụng trong Java

Các lớp như AtomicInteger, AtomicReference sử dụng CAS ở底层:

AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // thread-safe nhờ CAS
boolean updated = counter.compareAndSet(1, 2); // đổi 1 → 2 nếu hiện tại là 1

4. Ưu và nhược điểm

Ưu điểm: Không cần khóa, tránh chi phí chuyển ngữ cảnh luồng, hiệu suất cao khi xung đột thấp.

Nhược điểm:

  • Vấn đề ABA: Giá trị A → B → A khiến CAS tưởng không có thay đổi. Giải pháp: dùng AtomicStampedReference.
  • Tự quay vô hạn: Khi xung đột cao, CAS thất bại liên tục → tiêu tốn CPU.
  • Chỉ áp dụng cho một biến: Không hỗ trợ nguyên tử trên nhiều biến cùng lúc.

Ví dụ minh họa: Giả sử value = 5.

  1. Luồng A đọc old = 5, tính new = 6.
  2. Trước khi A thực hiện CAS, luồng B đã tăng value lên 6.
  3. A thực hiện CAS(5, 6): thất bại vì giá trị hiện tại là 6 ≠ 5.
  4. A lặp lại: đọc lại old = 6, tính new = 7, thử CAS(6, 7) — thành công nếu không có can thiệp.

Đoạn mã minh họa từ AtomicInteger

public final int getAndIncrement() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next)) {
            return current;
        }
        // Thất bại → lặp lại
    }
}

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

Cách giảm thiểu tự quay vô hạn trong CAS

  1. Lùi dần theo hàm mũ (Exponential Backoff):
    int attempts = 0;
    while (!attemptCAS()) {
        attempts++;
        if (attempts > MAX_RETRY) {
            fallbackToBlockingLock();
            break;
        }
        Thread.sleep(Math.min(100, 1 << attempts));
    }
  2. Thêm ngẫu nhiên vào thời gian chờ:
    while (!attemptCAS()) {
        Thread.sleep(ThreadLocalRandom.current().nextInt(10, 100));
    }
  3. Chiến lược lai: Tự quay vài lần → Thread.yield() → chuyển sang hàng đợi treo (như AQS).
  4. Giới hạn số lần tự quay:
    for (int i = 0; i < MAX_SPINS; i++) {
        if (attemptCAS()) return true;
    }
    // Chuyển sang cơ chế chặn

Xử lý thực tế trong Java

Các lớp AtomicInteger không tích hợp cơ chế chống tự quay vô hạn. Chúng dựa vào:

  • Tính công bằng của bộ lập lịch luồng JVM/OS.
  • Giả định rằng xung đột cực đoan hiếm xảy ra.
  • Khuyến nghị chuyển sang synchronized hoặc ReentrantLock nếu xung đột cao.

Do đó, AQS chọn cách treo luồng thay vì tự quay — đánh đổi hiệu suất tức thì để đạt tính công bằng và ổn định lâu dài.

Phương thức Có tự quay? Mục đích Kiểu trả về
compareAndSet(expect, update) Không Thử CAS một lần boolean
getAndIncrement() Tăng nguyên tử, trả về giá trị cũ int
incrementAndGet() Tăng nguyên tử, trả về giá trị mới int
getAndAdd(delta) Cộng delta nguyên tử, trả về giá trị cũ int
weakCompareAndSet() Không CAS với ngữ nghĩa bộ nhớ yếu boolean

Thẻ: CAS AQS Java Concurrency AtomicInteger ReentrantLock

Đăng vào ngày 31 tháng 8 lúc 02:28