Synchronized là cơ chế đồng bộ hóa tích hợp trong ngôn ngữ Java, cung cấp quyền truy cập độc quyền vào các phần quan trọng của mã nguồn. Theo Oracle, 2024, bổ từ synchronized đảm bảo rằng chỉ một luồng có thể thực thi phương thức hoặc khối lệnh được đánh dấu tại một thời điểm cụ thể. Cơ chế này dựa trên monitor — một khái niệm cơ bản của hệ điều hành, đảm bảo hoạt động chính xác của các ứng dụng đa luồng ở mọi cấp độ phức tạp.
Những điểm chính
Synchronized là một từ khóa trong Java đảm bảo rằng chỉ một luồng thực thi một phần mã được bảo vệ tại một thời điểm, ngăn chặn hỏng dữ liệu khi truy cập đồng thời. Nó xuất hiện trong phiên bản đầu tiên của Java và vẫn là cách đơn giản nhất để đảm bảo an toàn luồng cho các nhà phát triển ở mọi trình độ.
Bổ từ synchronized giải quyết hai nhiệm vụ: loại trừ lẫn nhau (mutual exclusion) và khả năng hiển thị thay đổi (visibility). Khi một luồng thoát khỏi khối synchronized, tất cả các thay đổi được đảm bảo hiển thị cho các luồng khác khi vào khối được đồng bộ hóa trên cùng một đối tượng.
Synchronized có thể được áp dụng cho toàn bộ phương thức hoặc cho một khối mã tùy ý với việc chỉ định đối tượng monitor. Trong cả hai trường hợp, JVM chèn các lệnh monitorenter và monitorexit ở cấp độ bytecode.
Trong các ứng dụng đa luồng không có đồng bộ hóa, điều kiện tranh đua (race condition) xảy ra khi hai luồng cùng sửa đổi dữ liệu, dẫn đến kết quả không thể đoán trước. Synchronized trở thành công cụ đầu tiên và chính của Java để giải quyết vấn đề này, cung cấp cú pháp khai báo đơn giản mà bất kỳ nhà phát triển nào cũng có thể sử dụng.
Cơ chế synchronized dựa trên khái niệm monitor — một nguyên thủy đồng bộ hóa cấp cao được tích hợp trong mọi đối tượng Java. Monitor được liên kết với một đối tượng khi khối synchronized được sử dụng lần đầu trên đối tượng đó.
Mọi đối tượng trong Java đều có một monitor liên kết. Khi một luồng vào khối synchronized, nó chiếm giữ monitor của đối tượng. Nếu monitor đã bị chiếm bởi một luồng khác, luồng đó sẽ bị chặn cho đến khi được giải phóng. Trong bytecode, điều này tương ứng với cặp lệnh monitorenter và monitorexit.
JVM tối ưu hóa synchronized qua nhiều cấp độ: biased locking (khóa thiên vị) cho truy cập đơn luồng, lightweight locking (khóa nhẹ) cho tranh chấp thấp, và heavyweight locking (khóa nặng) cho tranh chấp cao với sự tham gia của hệ điều hành. Các cấp độ này cải thiện hiệu suất mà không thay đổi mã.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
Synchronized thiết lập mối quan hệ happens-before: tất cả các hành động trong một luồng trước khi thoát khỏi khối synchronized đều hiển thị với luồng khác sau khi vào khối được đồng bộ hóa trên cùng một đối tượng. Điều này đảm bảo không chỉ loại trừ lẫn nhau mà còn tính nhất quán dữ liệu cho tất cả các luồng.
Java cung cấp hai cách áp dụng synchronized: ở cấp độ phương thức và ở cấp độ khối lệnh. Lựa chọn giữa chúng ảnh hưởng đến hiệu suất và độ chi tiết của đồng bộ hóa.
Đánh dấu một phương thức với bổ từ synchronized sẽ tự động đồng bộ hóa nó trên thể hiện hiện tại (đối với phương thức thể hiện) hoặc trên đối tượng Class (đối với phương thức tĩnh). Đây là cách đơn giản nhất để đảm bảo loại trừ lẫn nhau, nhưng thường là quá mức nếu phần quan trọng chỉ chiếm một phần nhỏ của phương thức và phần còn lại không cần đồng bộ hóa.
Khối lệnh synchronized cho phép kiểm soát chính xác: bạn chỉ định đối tượng monitor và đồng bộ hóa chỉ phần mã cần thiết, để phần còn lại của phương thức bên ngoài khóa. Điều này giảm thiểu thời gian giữ monitor và cải thiện hiệu suất tổng thể của ứng dụng trong môi trường đa luồng, vì các luồng khác có thể thực thi mã không liên quan song song mà không cần đợi giải phóng monitor.
class DataProcessor {
private final Object lock = new Object();
public void process() {
// mã bên ngoài phần quan trọng - không đồng bộ hóa
prepareData()
synchronized (lock) {
// chỉ khối này được bảo vệ
updateSharedState()
}
// tiếp tục không khóa
cleanup()
}
}
| Tiêu chí | Phương thức synchronized | Khối lệnh synchronized |
|---|---|---|
| Monitor | this (thể hiện) hoặc Class | bất kỳ đối tượng nào |
| Độ chi tiết | toàn bộ phương thức | chỉ mã cần thiết |
| Tính dễ đọc | cao | trung bình |
| Hiệu suất | thấp hơn với phương thức lớn | cao hơn với phần quan trọng nhỏ |
Trong phát triển Android, synchronized được sử dụng rộng rãi để bảo vệ SharedPreferences, truy cập cơ sở dữ liệu và các thành phần giao diện. Tuy nhiên, việc sử dụng nó trên luồng chính bị khuyến cáo mạnh mẽ do nguy cơ đóng băng giao diện.
SharedPreferences trong Android cung cấp an toàn luồng cơ bản, nhưng khi chỉnh sửa từ nhiều luồng qua Editor, có thể cần đồng bộ hóa bên ngoài. Một khối synchronized với đối tượng khóa riêng biệt đảm bảo tính nhất quán của các thay đổi.
class PreferencesManager(private val prefs: SharedPreferences) {
private val lock = Any()
fun writeToken(token: String) {
synchronized (lock) {
prefs.edit()
.putString("auth_token", token)
.apply()
}
}
}
Hạn chế chính của synchronized trên Android là chặn luồng. Không giống như coroutine với Mutex, synchronized chặn toàn bộ luồng hệ thống. Trên luồng chính, điều này gây ra ANR. Trong phát triển Android hiện đại, nên thay thế synchronized bằng coroutine (suspend Mutex) hoặc các kiểu nguyên tử (AtomicInteger).
Java và Kotlin hiện đại cung cấp một số giải pháp thay thế cho synchronized, mỗi giải pháp giải quyết cùng vấn đề với ít hạn chế hơn hoặc hiệu suất tốt hơn.
Giao diện Lock với các triển khai ReentrantLock và ReadWriteLock cung cấp thời gian chờ, chờ có thể ngắt và nhiều hàng đợi Condition. Nó linh hoạt hơn synchronized nhưng yêu cầu giải phóng rõ ràng trong finally, làm tăng nguy cơ lỗi khi quên unlock.
AtomicInteger, AtomicLong, AtomicReference và các lớp khác sử dụng thuật toán Lock-Free dựa trên CAS (Compare-And-Swap). Chúng nhanh hơn đáng kể so với synchronized trong các kịch bản tranh chấp vừa phải vì không chặn luồng mà thực hiện thử lại lạc quan mà không cần chuyển đổi ngữ cảnh hạt nhân hệ điều hành.
ThreadLocal cung cấp một cách tiếp cận thay thế: mỗi biến ThreadLocal được cô lập trong một luồng duy nhất và không yêu cầu đồng bộ hóa cho việc đọc và ghi. Điều này loại bỏ hoàn toàn nhu cầu về synchronized cho dữ liệu không nên chia sẻ giữa các luồng. ThreadLocal được sử dụng tích cực trong các framework (Spring, Hibernate) để lưu trữ ngữ cảnh giao dịch và phiên làm việc.
Trong các dự án Kotlin cho Android, giải pháp thay thế cho synchronized là Mutex từ kotlinx.coroutines. Nó không chặn luồng hệ điều hành mà tạm dừng coroutine cho đến khi khóa được giải phóng — điều này cho phép sử dụng hiệu quả các luồng trong pool và tránh ANR khi chờ giải phóng tài nguyên lâu.
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
val mutex = Mutex()
var counter = 0
suspend fun safeIncrement() {
mutex.withLock {
counter++
}
}
Hiệu suất của synchronized đã thay đổi đáng kể trong các phiên bản Java gần đây. Trước đây nó được coi là cơ chế “nặng”, nhưng các JVM hiện đại đã loại bỏ hầu hết chi phí nhờ các tối ưu hóa nâng cao của trình biên dịch JIT. Hãy xem chi tiết cách máy ảo tăng tốc mã đồng bộ hóa trong thời gian chạy.
Trình biên dịch JIT của JVM áp dụng một số tối ưu hóa: biased locking loại bỏ đồng bộ hóa nếu khóa luôn được chiếm giữ bởi cùng một luồng; lock coarsening hợp nhất các khối synchronized liền kề thành một; lock elimination xóa đồng bộ hóa nếu đối tượng chỉ có thể truy cập bởi một luồng. Các tối ưu hóa này làm cho synchronized hầu như miễn phí khi tranh chấp thấp.
JVM xác định mức độ tranh chấp cho mỗi đối tượng: khi không có tranh chấp, biased locking được kích hoạt; khi luồng thứ hai xuất hiện, khóa chuyển sang chế độ nhẹ với chờ spin; và chỉ khi chờ đợi kéo dài, nó mới leo thang lên chế độ nặng với mutex hệ thống. Sự leo thang này xảy ra tự động và nhà phát triển không cần chọn chiến lược thủ công.
Trong các benchmark hiện đại (Java 17+), synchronized cho thấy hiệu suất tương đương với ReentrantLock ở mức tranh chấp thấp và trung bình. Ở mức tranh chấp cao, Lock có thể có lợi thế nhờ hàng đợi hiệu quả hơn với hỗ trợ timeout và ngắt. Đối với hệ thống tải cao nơi tranh chấp là liên tục, ReentrantLock với chế độ fair mang lại hành vi dự đoán được hơn.
Các lớp nguyên tử (AtomicInteger, AtomicReference) vẫn là nhanh nhất cho bộ đếm và cờ đơn giản nhờ triển khai Lock-Free dựa trên CAS. Chúng hoàn toàn không chặn luồng — khi xung đột, thao tác chỉ đơn giản thử lại trong một vòng lặp. Điều này mang lại lợi thế hiệu suất gấp 3-5 lần so với synchronized trên các thao tác tăng bộ đếm với 4-8 luồng.
Câu hỏi thường gặp
Synchronized cung cấp cả loại trừ lẫn nhau và khả năng hiển thị. Volatile chỉ đảm bảo khả năng hiển thị thay đổi — ghi vào biến volatile hiển thị cho tất cả luồng nhưng không ngăn chặn sửa đổi đồng thời, nghĩa là không bảo vệ khỏi điều kiện tranh đua.
Có, deadlock có thể xảy ra với đồng bộ hóa lồng nhau sử dụng thứ tự monitor khác nhau. Ví dụ, một luồng gọi synchronized(a) { synchronized(b) }, trong khi luồng khác gọi synchronized(b) { synchronized(a) }. Tránh các khối synchronized lồng nhau hoặc cố định một thứ tự monitor nhất quán.
Monitor là một cơ chế đồng bộ hóa liên kết với mọi đối tượng Java. Nó đảm bảo rằng chỉ một luồng thực thi mã synchronized trên đối tượng đó. Monitor bao gồm một khóa, một hàng đợi chờ và một tập hợp các luồng đang chờ thông báo qua wait/notify.
Trong các phiên bản Java hiện đại (17+), synchronized không thua kém Lock về hiệu suất nhờ các tối ưu hóa JIT (biased locking, lock coarsening). Lock được ưa chuộng không phải vì tốc độ mà vì các khả năng bổ sung: timeout, chờ có thể ngắt và nhiều hàng đợi Condition.
Phương thức tĩnh synchronized sử dụng monitor của đối tượng Class của lớp được chỉ định, không phải của thể hiện. Điều này có nghĩa là đồng bộ hóa áp dụng cho tất cả các thể hiện của lớp. Các phương thức không tĩnh và tĩnh synchronized sử dụng các monitor khác nhau và không chặn lẫn nhau.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm