Backend

99 Ngày Java — Ngày 62: Race condition và synchronized

SSite Admin
27 tháng 09, 2026 3 phút đọc 1 lượt xem
99 Ngày Java — Ngày 62: Race condition và synchronized

Hai luồng cùng tăng một biến đếm có thể cho kết quả nhỏ hơn mong đợi dù mỗi luồng đều chạy xong. Đây là race condition: kết quả phụ thuộc vào thứ tự xen kẽ các thao tác. Trên Java 21, ta bắt đầu với synchronized trước khi tìm đến các công cụ đồng thời phức tạp hơn.

Race condition và synchronized

1. counter++ không phải một bước

Phép tăng gồm đọc giá trị, cộng một rồi ghi lại. Hai luồng có thể cùng đọc 7, cùng ghi 8: một lượt tăng bị mất. Thread.join() chỉ đợi luồng kết thúc, không làm các phép tăng trước đó trở nên nguyên tử. Đừng viết bài kiểm tra đòi lỗi phải xuất hiện ở mọi lần chạy; lịch chạy khác nhau giữa các máy.

class UnsafeCounter {
    int value;
    void increment() { value++; } // read → add → write
}
// Run many increment tasks; the final value may be below the task count.

2. Khóa đúng dữ liệu bằng synchronized

Mỗi object có intrinsic lock (monitor). Một luồng vào synchronized trên cùng object sẽ loại trừ các luồng khác đang cần khóa ấy. Khi rời vùng khóa, Java thiết lập quan hệ happens-before với lần lấy khóa tiếp theo, nên thay đổi bên trong có thể được nhìn thấy đúng cách. Mọi đường đọc và ghi cùng một invariant phải tuân thủ cùng quy tắc khóa.

final class Counter {
    private int value;
    synchronized void increment() { value++; }
    synchronized int get() { return value; }
}
// For these instance methods, the monitor is this.

3. Method, block và phạm vi khóa

synchronized trên instance method khóa this; static synchronized khóa Class object. Block cho phép chọn một khóa riêng, nhưng mọi thao tác liên quan phải dùng chính object đó. Giữ vùng khóa ngắn: gọi mạng, sleep hoặc chờ Future trong khóa sẽ giữ các luồng khác lại mà không giúp bảo vệ dữ liệu.

final class Inventory {
    private final Object lock = new Object();
    private int stock = 5;
    boolean reserve() {
        synchronized (lock) {
            if (stock == 0) return false;
            stock--; return true;
        }
    }
}

4. Những giới hạn cần nhớ

Khóa trên String literal hoặc object công khai dễ bị mã khác chiếm ngoài ý muốn. Hai instance Counter có hai khóa khác nhau, nên synchronized instance method không bảo vệ biến static dùng chung. Nếu nhiều khóa được lấy theo thứ tự khác nhau, deadlock có thể xảy ra; ngày 66 sẽ đi sâu hơn.

  • Bài tập: chạy 10.000 lượt tăng bằng ExecutorService, so sánh Counter có và không có synchronized.

  • Bài tập: bỏ synchronized khỏi get(), rồi giải thích vì sao đọc giá trị trong khi các tác vụ vẫn chạy không còn cùng hợp đồng đồng bộ.

synchronized vừa loại trừ lẫn nhau vừa tạo bảo đảm về visibility khi mọi bên cùng dùng khóa. Nếu chỉ cần một bộ đếm độc lập, ngày 63 sẽ giới thiệu AtomicInteger; nếu nhiều trường phải thay đổi cùng nhau, một khóa bảo vệ cả invariant vẫn là cách dễ kiểm chứng.

Tài liệu đối chiếu

  • https://docs.oracle.com/javase/specs/jls/se21/html/jls-17.html

  • https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/package-summary.html

S

Site Admin

Engineer and writer. Building things with TypeScript and distributed systems.

Bình luận (0)

Bạn cần đăng nhập bằng Google để bình luận.

Hãy là người bình luận đầu tiên.

Bài viết liên quan

99 Ngày Spring — Ngày 62: Custom annotation kết hợp AOP

Tạo @Timed bằng metadata runtime, chọn method với @annotation, đo thời gian bằng @Around và giữ nguyên kết quả, lỗi.

27 thg 9, 20262 phút0
99 Ngày Spring — Ngày 61: AOP

Aspect, pointcut, advice, đo thời gian bằng Around và giới hạn của Spring proxy.

26 thg 9, 20267 phút8
99 Ngày Java — Ngày 61: Thread cơ bản

Thread và Runnable, vòng đời, start khác run, sleep khác join và interrupt để dừng hợp tác.

26 thg 9, 20266 phút5