volatile và synchronized: hai cơ chế đồng bộ nội tại Java
Hai cơ chế đồng bộ nội tại của Java: volatile chỉ lo visibility, synchronized cho cả loại trừ lẫn nhau lẫn visibility, với mark word và thin/fat lock bên dưới.
TL;DR: Khi dữ liệu vừa shared vừa mutable, Java cài sẵn hai cơ chế đồng bộ ở hai đầu của phổ. volatile chỉ lo visibility qua happens-before — đúng cho cờ trạng thái và immutable holder một writer, vô dụng với read-modify-write; nó cũng chặn torn read của long/double. synchronized cho cả mutual exclusion lẫn visibility, reentrant, tự nhả kể cả khi exception. Bên dưới, JVM giữ lock state trong mark word của object header và chỉ inflate lên OS monitor khi có tranh chấp thật, nên khóa không tranh chấp gần như chỉ một CAS. Cạm bẫy: tưởng volatile làm ++ nguyên tử, quên rằng reader cũng phải khóa, và để lock scope quá rộng hoặc quá hẹp.
1. Khi dữ liệu vừa shared vừa mutable
Confinement triệt tính "shared", immutability triệt tính "mutable" — cả hai làm vấn đề biến mất, nhưng chỉ dùng được khi ta có quyền chọn không chia sẻ hoặc không thay đổi. Nhiều trạng thái không cho ta quyền đó: số vé đã bán của một sự kiện trong TicketFlow phải được nhiều thread cùng thấy và tăng dần theo từng booking — vừa shared vừa mutable, không thể giam, không thể đóng băng. Rơi vào ô đó, chỉ còn một đường: canh gác mọi truy cập bằng synchronization.
Java cài sẵn hai cơ chế đồng bộ ngay trong ngôn ngữ, ở hai đầu một phổ: volatile nhẹ nhất — chỉ lo visibility, không lo loại trừ lẫn nhau; synchronized đầy đủ — cả loại trừ lẫn nhau lẫn visibility. Hiểu mỗi cái bảo đảm gì, và quan trọng hơn là không bảo đảm gì, mới chọn đúng công cụ thay vì rải khóa theo cảm tính. Ta đi từ nhẹ tới nặng.
2. volatile — recap nhanh và ranh giới cứng
Bài Visibility đã mổ volatile đủ sâu; đây chỉ recap để so với synchronized. Theo happens-before: một lần ghi biến volatile happens-before mọi lần đọc sau đó của chính biến ấy — khi B đọc thấy giá trị A vừa ghi, mọi thứ A viết trước lúc ghi cũng hiển thị với B (reader đọc biến thường không được hưởng gì). Nó chỉ lo visibility và ordering, không lo loại trừ lẫn nhau.
Use case đúng kiểu là cờ trạng thái một thread bật, nhiều thread đọc:
public class SalesGate {
private volatile boolean salesOpen = true; // admin ghi, nhieu worker doc
public void closeSales() { salesOpen = false; }
public boolean isOpen() { return salesOpen; }
}
Thước đo khi nào được dùng vẫn là checklist ba điều kiện của bài Visibility: (1) lệnh ghi không phụ thuộc giá trị hiện tại của biến, hoặc chỉ một thread duy nhất ghi; (2) biến không tham gia invariant nào với biến khác; (3) không cần khóa vì lý do nào khác. salesOpen thỏa cả ba — ghi nó là gán thẳng một hằng. Một tham chiếu volatile trỏ tới immutable holder (mẫu PriceBoard của bài Immutability) cũng thỏa, miễn đúng một thread công bố.
Còn sold của TicketFlow vi phạm ngay điều đầu — đó là ranh giới cứng. sold++ là một read-modify-write ba bước rời rạc; volatile bảo đảm từng lần đọc lẻ, ghi lẻ đúng, nhưng không ngăn hai thread chen nhau giữa cụm ba bước — cùng đọc 9, cùng cộng thành 10, cùng ghi 10, một lần tăng bốc hơi. Với sold, volatile vô dụng; ta cần một cơ chế quây cả cụm thao tác thành một khối. Đó là synchronized.
2.1 Ranh giới còn lại: torn read của long và double
Có đúng một trường hợp volatile lo cả tính nguyên tử của một lần truy cập đơn lẻ. (Không độc quyền của nó: synchronized, AtomicLong, hay một field final được công bố an toàn cũng chặn được hiện tượng dưới đây — volatile chỉ là cách rẻ nhất.)
Java Memory Model bảo đảm mọi lần đọc/ghi một biến đều lấy ra một giá trị ai đó đã thực sự ghi — kể cả biến reference rộng 64 bit. Ngoại lệ duy nhất: long và double không khai volatile. Với chúng, JLS §17.7 cho phép JVM tách một lần ghi 64-bit thành hai lần ghi 32-bit. Thread đọc khi đó có thể ghép 32 bit cao của lần ghi này với 32 bit thấp của lần ghi khác, thu về một con số chưa ai từng ghi — gọi là torn read/write.
private long revenue; // co the bi torn read
private volatile long revenueOk; // doc/ghi luon nguyen tu
Trên phần cứng 64-bit ngày nay JVM hầu như luôn ghi nguyên tử nên lỗi rất hiếm, nhưng tới tận JLS SE 25 §17.7 nó vẫn nằm trong đặc tả: code dựa vào tính nguyên tử của một long chia sẻ là sai về nguyên tắc — đúng trên máy hôm nay, hỏng trên kiến trúc khác, gần như không tái hiện được khi debug. Đừng nhầm với atomicity của phép cập nhật: revenue += amount vẫn là read-modify-write ba bước mà volatile không cứu — đúng ranh giới cứng vừa nêu.
3. synchronized — intrinsic lock
3.1 Mọi object đều mang một monitor
book của TicketFlow cần cả hai thứ cùng lúc — atomicity cho cụm check-then-act (bài Atomicity) và visibility cho mọi reader (bài Visibility) — mà volatile chỉ cho được vế sau. Mảnh còn thiếu là synchronized, cơ chế đồng bộ đầy đủ Java gắn thẳng vào ngôn ngữ.
Mỗi đối tượng Java ngầm mang một khóa — intrinsic lock (monitor lock) — gắn vào chính header của object, không cần khai báo. Một khối synchronized mượn khóa đó để dựng vùng tới hạn:
synchronized (lock) {
// vung toi han: tai mot thoi diem nhieu nhat mot thread vao duoc
}
Vào khối, thread tự động giành lock; rời khối, nó tự nhả — dù rời bình thường hay vì một exception. Chính cái "tự nhả kể cả khi exception" này khiến synchronized an toàn hơn các khóa tường minh khi code có thể ném lỗi giữa chừng. Intrinsic lock hoạt động như mutex - mutual exclusion lock: nhiều nhất một thread giữ được; A muốn giành khóa đang trong tay B thì phải chờ, bị block, tới khi B nhả. Vì chỉ một thread chạy được vùng code một khóa canh giữ, các khối synchronized cùng khóa thực thi nguyên tử so với nhau — không thread nào quan sát được thread khác đang ở giữa chừng một khối cùng khóa.
Một synchronized method chỉ là cú pháp tắt cho khối synchronized ôm trọn thân method, khóa trên this (instance method) hoặc trên đối tượng Class (static method). Hai dạng sau tương đương:
public synchronized void f() { /* ... */ }
public void f() { synchronized (this) { /* ... */ } }
3.2 synchronized lo cả atomicity lẫn visibility
synchronized mạnh hơn volatile vì giải quyết cả hai vấn đề nền tảng cùng lúc. Phần atomicity đến từ loại trừ lẫn nhau: vì chỉ một thread vào được vùng tới hạn, cả cụm compound action bên trong khối diễn ra như một khối không ai chen được. Đây đúng thứ volatile thiếu. Phần visibility đến từ một quy tắc happens-before khác: nhả một khóa happens-before giành lại chính khóa đó. Khi A rời khối synchronized, mọi thay đổi nó làm bên trong hiển thị với B khi B vào khối synchronized cùng khóa. Khóa vừa ngăn hai thread vào cùng lúc, vừa bắc một cây cầu bộ nhớ, y như cặp ghi-đọc volatile.
Hệ quả hay bị bỏ quên: reader cũng phải khóa. Nếu một biến được canh bằng khóa thì mọi đường truy cập tới nó, đọc lẫn ghi, đều phải giữ cùng khóa. Tưởng chỉ cần đồng bộ khi ghi là sai: reader không khóa vẫn có thể đọc trúng dữ liệu dở dang hoặc giá trị cũ vì không có cây cầu bộ nhớ nào bắc tới nó.
3.3 Reentrant — khóa tái nhập
Intrinsic lock tái nhập (reentrant): một thread đang giữ khóa mà lại gặp khối synchronized khác trên cùng khóa thì vào được luôn, không tự khóa chính mình. JVM cài bằng một bộ đếm số lần giành - acquisition count - và một thread sở hữu: thread sở hữu giành lại thì count tăng, rời mỗi khối thì count giảm, chỉ khi count về 0 khóa mới thực sự nhả cho thread khác. Khóa Java vì vậy cấp theo từng lần-giành-của-một-thread, không theo từng lần-giành tuyệt đối.
Reentrancy cứu ta khỏi deadlock trong một tình huống rất tự nhiên: method synchronized của lớp con gọi super cũng synchronized trên cùng đối tượng.
public class Widget {
public synchronized void doSomething() { /* ... */ }
}
public class LoggingWidget extends Widget {
public synchronized void doSomething() {
System.out.println(this + ": calling doSomething");
super.doSomething(); // gianh lai CUNG khoa tren this
}
}
Khi gọi LoggingWidget.doSomething, thread đã giữ khóa trên this; super.doSomething() lại cần đúng khóa đó. Không reentrant thì thread chờ một khóa chính nó đang giữ, treo vĩnh viễn. Vì reentrant, count tăng lên 2 rồi về 0, mọi thứ chạy trơn.
Khóa là một quy ước, không phải phép thuật. Giành khóa của một đối tượng không ngăn thread khác đọc/ghi field của nó; điều duy nhất nó ngăn là thread khác giành cùng cái khóa ấy. Không có liên hệ nội tại nào giữa intrinsic lock và các field — bảo vệ chỉ tồn tại khi mọi thread cùng tuân "chạm biến này thì phải giữ khóa kia".
4. Cơ chế bên dưới: synchronized có thật sự chậm không?
"Mọi object đều mang một khóa" nghe như mỗi object phải vác một cấu trúc đồng bộ của OS — mutex thật với hàng đợi và syscall. Thế thì synchronized đắt vô lý: chương trình Java tạo hàng triệu object, phần lớn không bao giờ bị khóa, hoặc bị khóa mà không hề tranh chấp. JVM giải bằng cách trả khóa theo nhu cầu: trạng thái khóa khởi đầu chỉ là vài bit trong header của object, chỉ "phình" thành cấu trúc đắt tiền khi có tranh chấp thật.
4.1 Mark word — nơi khóa sống
Mỗi object trên heap HotSpot mở đầu bằng một header, word đầu tiên gọi là mark word — một word 64-bit đa dụng. Tùy trạng thái, nó chứa identity hash code và tuổi GC, hoặc thông tin khóa: vài bit thấp là tag cho biết object ở trạng thái khóa nào, phần còn lại diễn giải theo tag đó. "Intrinsic lock gắn vào object" vì vậy đúng nghĩa đen — khóa là chính mấy chục bit nằm ngay đầu object, không phải một field ẩn trỏ đi đâu xa.
4.2 Thin lock — một cú CAS khi không ai tranh
Khi một thread vào khối synchronized mà khóa còn trống, JVM không hề gọi OS. Nó dựng một lock record nhỏ trên stack của thread, rồi dùng một lệnh compare-and-swap — CAS, lệnh so-sánh-rồi-ghi nguyên tử của CPU, nhân vật chính của bài kế tiếp — ghi con trỏ tới lock record đó vào mark word. CAS thành công nghĩa là thread đã sở hữu khóa. Toàn bộ chi phí là một lệnh CPU: không syscall, không context switch, không cấp phát heap. Trạng thái này gọi là thin lock — khóa mỏng. Rời khối, một CAS nữa trả mark word về cũ.
Đây là câu trả lời cho "synchronized có chậm không": với khóa không tranh chấp — trường hợp áp đảo trong code thật — chi phí gần như chỉ một CAS, rẻ hơn nhiều so với hình dung "khóa nghĩa là gọi OS". Lời khuyên đúng vì vậy không phải né synchronized, mà là né contention.
Trước đây HotSpot còn một tầng rẻ hơn nữa: biased locking — khóa "thiên vị" thread đầu tiên giành nó để các lần giành lại sau của chính nó khỏi tốn cả CAS. Tầng này tắt mặc định từ JDK 15 rồi gỡ hẳn (JEP 374): chi phí thu hồi bias vượt lợi ích khi một CAS không tranh chấp đã đủ rẻ.
4.3 Fat lock — inflate khi có tranh chấp
Chuyện đổi khác khi thread B CAS vào một mark word đang thuộc về A. CAS thất bại, JVM biết có tranh chấp và inflate — làm phình — cái khóa: dựng một ObjectMonitor đầy đủ trong bộ nhớ native rồi ghi con trỏ tới nó vào mark word. Monitor này mới là "khóa thật" với đủ bộ phận: thread owner, bộ đếm reentrancy (chính acquisition count ở mục 3.3), entry queue chứa các thread đang chờ giành khóa, và wait set chứa các thread đã gọi wait() — chủ đề bài 09. Thread thua cuộc vào entry queue và block qua primitive của OS; đây là lúc chi phí context switch xuất hiện. Owner nhả khóa thì OS đánh thức một thread trong queue dậy tranh tiếp.
flowchart TD
A[Thread vao khoi synchronized] --> B{Mark word con trong?}
B -- "con trong" --> C[CAS lock record vao mark word]
C -- "thanh cong" --> D[Thin lock: vao vung toi han, khong syscall]
C -- "that bai" --> E[Phat hien tranh chap]
B -- "da co chu" --> E
E --> F[Inflate: dung ObjectMonitor - fat lock]
F --> G[Thread thua vao entry queue, block qua OS]
D --> H[Roi khoi: CAS tra lai mark word]
G --> I[Owner nha khoa, OS danh thuc thread trong queue]
I --> D
style D fill:#6EE7B7
style E fill:#FCD34D
style F fill:#FCA5A5
style G fill:#FCA5A5Hai hệ quả thực dụng. Thứ nhất, chi phí synchronized không cố định mà phụ thuộc lịch sử tranh chấp của từng object: cùng một dòng code có thể tốn một CAS ở chỗ này, hai lần context switch ở chỗ khác. Thứ hai, mọi kỹ thuật giảm contention ở mục 5 — thu hẹp vùng khóa, tách khóa theo dữ liệu — đều nhằm một việc: giữ khóa đừng bao giờ phải inflate.
Spec / reference chính thức:
- JLS §17.1 — Synchronization — định nghĩa monitor, lock action và unlock action ở mức đặc tả ngôn ngữ.
- JEP 374 — Deprecate and Disable Biased Locking — lý do tầng biased locking bị gỡ: chi phí revocation vượt lợi ích trên CPU hiện đại.
Ghi chú: mark word và thin/fat lock là chi tiết cài đặt của HotSpot, không nằm trong JLS — spec chỉ đặc tả hành vi (mutual exclusion + happens-before), còn JVM được tự do chọn cách cài rẻ nhất thỏa hành vi đó.
Một liên hệ về phía trước: ObjectMonitor là code C++ trong JVM. Bài ReadWriteLock, StampedLock và AQS sẽ cho thấy java.util.concurrent dựng lại đúng bộ máy này — state là một biến int CAS được cộng một hàng đợi thread chờ — nhưng hoàn toàn ở tầng Java qua AbstractQueuedSynchronizer. Cùng một bài toán, hai tầng cài đặt.
5. Cạm bẫy và chi phí
synchronized đúng nhưng không miễn phí, và rất dễ dùng sai theo những kiểu không lộ ra trong test đơn luồng. Ba nhóm vấn đề đáng nắm trước khi sang các công cụ nhẹ hơn.
5.1 Lock scope quá rộng hay quá hẹp
Khóa quá rộng: một coarse-grained lock ôm trọn một service — kiểu Java Monitor Pattern ở bài kế tiếp — bắt hai khách đặt vé cho hai sự kiện khác nhau xếp hàng chờ nhau dù dữ liệu độc lập. Tệ hơn là giữ khóa qua việc dài hơi — một lần ghi log ra đĩa hay network call trong khối synchronized kéo thời gian giữ khóa từ nano lên mili giây, nhân contention lên hàng nghìn lần. Nguyên tắc: nhả khóa trước khi làm I/O hay bất kỳ việc chậm nào không cần khóa. Chiều ngược lại — khóa quá hẹp — còn nguy hơn vì âm thầm sai: tách một compound action thành nhiều khối nhỏ phá vỡ chính tính nguyên tử ta cần.
// SAI — hai khoi tach roi khong nguyen tu voi nhau
synchronized (lock) { current = sold.getOrDefault(eventId, 0); } // check
if (current < capacity)
synchronized (lock) { sold.put(eventId, current + 1); } // act, da qua muon
Giữa hai khối, một thread khác có thể chen vào đổi sold; quan sát current trở nên cũ ngay khi rời khối đầu. Cân bằng: giữ khóa đủ lâu để bao trọn cả compound action, nhưng nhả trước khi làm việc dài hơi không cần khóa.
5.2 Contention
Vì khóa serialize các thread, một khóa bị nhiều thread cùng tranh trở thành nút cổ chai. Càng nhiều core, càng nhiều thread dồn vào một khóa coarse-grained thì throughput càng đi ngang hoặc tụt, vì phần song song co lại còn phần tuần tự thì không. Thread thua cuộc tranh khóa phải block, kéo theo context switch của OS. Hai hướng giảm contention là chủ đề các bài sau: khóa chi tiết hơn — một khóa riêng cho mỗi sự kiện (per-event lock hoặc striped lock) để các sự kiện độc lập không chặn nhau; hoặc bỏ khóa hẳn cho thao tác trên một biến, dùng CAS lock-free — cánh cửa mở sang bài Atomic & CAS.
5.3 Deadlock
Khi một thread cần giữ nhiều hơn một khóa, xuất hiện rủi ro deadlock: hai thread chờ nhau vĩnh viễn, mỗi bên giữ một khóa bên kia cần.
// Thread A: chuyen tu tai khoan x sang y
synchronized (x) { synchronized (y) { /* ... */ } }
// Thread B: chuyen tu y sang x — thu tu gianh khoa NGUOC lai
synchronized (y) { synchronized (x) { /* ... */ } }
Nếu A giành được x và B giành được y cùng lúc, A chờ y mãi còn B chờ x mãi — cả hai treo, không exception, không log, ứng dụng đứng im. Cách phòng cơ bản và hiệu quả nhất là áp một thứ tự giành khóa toàn cục nhất quán: mọi thread luôn giành khóa theo cùng một thứ tự, ví dụ sắp theo một id ổn định của object; khi đó không thể có chu trình chờ, và deadlock kiểu này biến mất. Deadlock và các liveness hazard khác sẽ trở lại với nhiều sắc thái hơn khi ta cầm các khóa tường minh ở bài ReentrantLock và Condition — nơi tryLock có timeout cho một lối thoát mà synchronized không có.
6. Liên hệ các bài khác
- Bài 09 — Guarded block với wait/notify: phần tiếp theo trực tiếp — đặt intrinsic lock của bài này vào Java Monitor Pattern, rồi mở rộng monitor để một thread chờ điều kiện bằng
wait/notify. - Bài 03 — Thread safety: nguồn của hai vấn đề atomicity + visibility mà
synchronizedgiải quyết trọn trong một cơ chế duy nhất. - Bài 07 — Immutability: immutable holder + tham chiếu volatile là mẫu "né khóa" khi chỉ một writer; bài này là lời giải khi có nhiều writer cùng ghi.
- Bài 10 — Atomic & CAS: chính lệnh CAS mà JVM dùng cho thin lock, phơi ra cho code ứng dụng — đường thoát khỏi khóa cho thao tác trên một biến.
- Bài 12 — ReadWriteLock, StampedLock và AQS: AQS dựng lại bộ máy monitor (state CAS được + hàng đợi thread chờ) ở tầng Java, làm nền cho mọi lock thư viện.
- Bài 13 — Delegation & concurrent collections: kỹ thuật client-side locking dựa trên khóa public, và vì sao nó mong manh.
7. Tóm tắt
volatilelà synchronization nhẹ nhất: chỉ bảo đảm visibility và ordering qua happens-before, không loại trừ lẫn nhau. Đúng cho cờ trạng thái độc lập và tham chiếu trỏ tới immutable holder một-writer; vô dụng ngay khi thao tác là read-modify-write hoặc biến dính invariant với biến khác. Nó cũng chặn torn read củalong/double(JLS §17.7) — nhưng đó là atomicity của một lần truy cập, không phải của cả cụm.synchronizedlà synchronization đầy đủ: intrinsic lock của mỗi object cho loại trừ lẫn nhau cộng visibility cùng lúc, reentrant, nhả tự động kể cả khi exception. Không tranh chấp thì gần như chỉ một CAS (thin lock), chỉ inflate lênObjectMonitorkhi có tranh chấp thật.synchronizedkhông miễn phí: khóa quá hẹp phá vỡ atomicity của compound action, contention biến khóa thành nút cổ chai, và giữ nhiều khóa sai thứ tự gây deadlock — phòng bằng một thứ tự giành khóa nhất quán.
Có synchronized là có loại trừ lẫn nhau, nhưng còn hai câu hỏi bỏ ngỏ: đặt khóa ở đâu cho gọn, và làm sao để một thread chờ một điều kiện thành đúng trước khi tiếp. Cả hai là chủ đề bài kế tiếp: Guarded block với wait/notify trên Java Monitor Pattern.
8. Tự kiểm tra
Q1Một synchronized method static khóa cái gì? Thread đang chạy nó có chặn được thread khác chạy synchronized instance method của cùng class không?▸
Method static synchronized khóa trên đối tượng Class của class đó (ví dụ BookingService.class), còn instance method synchronized khóa trên this — hai object hoàn toàn khác nhau, tức hai khóa khác nhau.
Vì vậy chúng không loại trừ lẫn nhau: một thread chạy method static và một thread chạy instance method có thể chạy song song, kể cả khi cả hai cùng chạm vào một static field. Nếu hai loại method cùng canh một dữ liệu, đó là bug — phải thống nhất một khóa duy nhất cho dữ liệu đó.
Q2Reader chỉ đọc một biến, không ghi gì — vì sao vẫn phải synchronized trên cùng khóa với writer?▸
Vì synchronized có hai bảo đảm, và reader cần vế thứ hai: visibility. Quy tắc happens-before của khóa là nhả khóa happens-before giành lại chính khóa đó — cây cầu bộ nhớ chỉ bắc giữa hai thread cùng đi qua một khóa.
Reader không khóa thì không có cây cầu nào bắc tới nó: nó có thể đọc trúng giá trị cũ bị cache, hoặc trạng thái dở dang giữa chừng một compound action của writer. Đây là lỗi âm thầm vì test đơn luồng và cả phần lớn test đa luồng đều không lộ.
Q3Vì sao synchronized gần như miễn phí khi không có tranh chấp? Điều gì xảy ra ở lần tranh chấp đầu tiên?▸
Khi khóa còn trống, JVM chỉ thực hiện một lệnh CAS ghi con trỏ lock record vào mark word của object — trạng thái thin lock. Không syscall, không context switch, không cấp phát monitor; chi phí cỡ một lệnh CPU.
Khi một thread CAS thất bại vì khóa đã có chủ, JVM inflate khóa: dựng ObjectMonitor đầy đủ (owner, reentrancy count, entry queue, wait set) trong bộ nhớ native và ghi con trỏ vào mark word. Từ đó thread thua phải block qua hệ điều hành — chi phí nhảy từ một lệnh CPU lên hai lần context switch. Vì vậy thứ cần né không phải synchronized mà là contention.
Q4Khi nào volatile là đủ thay cho synchronized? Vì sao volatile int sold với sold++ vẫn sai dù mọi lần đọc đều thấy giá trị mới nhất?▸
volatile đủ khi cả ba điều kiện cùng đúng: ghi không phụ thuộc giá trị hiện tại của biến (hoặc chỉ một thread ghi), biến không dính invariant với biến khác, và không cần khóa vì lý do nào khác. Cờ trạng thái và tham chiếu trỏ tới immutable holder một-writer là hai use case chuẩn.
sold++ sai vì nó là read-modify-write ba bước: đọc, cộng, ghi. volatile bảo đảm từng lần đọc lẻ và ghi lẻ tôn trọng synchronization order, nhưng không gói ba bước thành một khối nguyên tử — hai thread vẫn cùng đọc 9, cùng ghi 10, mất một lần tăng. Visibility không thay được atomicity.
Q5Intrinsic lock là reentrant nghĩa là gì? Cho một tình huống sẽ deadlock nếu nó không reentrant.▸
Reentrant nghĩa là một thread đang giữ khóa mà gặp lại một khối synchronized khác trên cùng khóa thì vào được luôn, không tự chặn mình. JVM cài bằng một acquisition count cộng một thread sở hữu: thread sở hữu giành lại thì count tăng, rời mỗi khối thì count giảm, chỉ khi count về 0 khóa mới thực sự nhả cho thread khác.
Tình huống kinh điển: method synchronized của lớp con gọi super.doSomething() cũng synchronized trên cùng this. Thread đã giữ khóa trên this; nếu khóa không reentrant, lời gọi super sẽ chờ một khóa do chính thread đó đang giữ — treo vĩnh viễn. Vì reentrant, count chỉ tăng lên 2 rồi về 0, mọi thứ chạy trơn.
Q6long không khai volatile có thể bị torn read — hiện tượng đó là gì, và vì sao int thì không dính?▸
JMM cho phép JVM tách một lần ghi 64-bit của long/double không volatile thành hai lần ghi 32-bit (JLS §17.7). Một thread đọc chen vào giữa có thể ghép 32 bit cao của lần ghi này với 32 bit thấp của lần ghi khác, thu về một con số chưa ai từng ghi — torn read.
int và mọi kiểu rộng nhiều nhất 32 bit (kể cả reference) luôn được đọc/ghi nguyên tử nên không dính. Chỉ long/double mới cần bảo hộ, và volatile là cách rẻ nhất — synchronized, AtomicLong, hay field final công bố an toàn cũng chặn được. Lưu ý đây là atomicity của một lần truy cập; revenue += amount vẫn là read-modify-write mà volatile không cứu.
Q7Vì sao chia một compound action thành nhiều khối synchronized nhỏ lại nguy hiểm hơn một khối quá rộng?▸
Khóa quá rộng chỉ hại hiệu năng — nó bắt các thread chờ nhau lâu hơn cần thiết, nhưng chương trình vẫn đúng. Khóa quá hẹp thì âm thầm sai: tách bước kiểm tra và bước hành động thành hai khối riêng để lại một khe giữa chúng mà thread khác chen vào được.
Ví dụ đọc current = sold.get() trong một khối rồi sold.put(current + 1) trong khối sau: giữa hai khối, một thread khác có thể đổi sold, khiến current trở nên cũ ngay khi rời khối đầu — đúng thứ atomicity cần bảo vệ bị phá. Nguyên tắc: giữ khóa đủ lâu để bao trọn cả compound action, nhưng nhả trước khi làm I/O hay việc chậm không cần khóa.
Bài tiếp theo: Guarded block với wait/notify trên Java Monitor Pattern
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên