Lock: nó là gì, các loại khóa và cách sử dụng trong đồng bộ hóa

Tác giả: IT Sectr Đã đăng: 2026-03-19 Thời gian đọc: 8 phút

Lock là cơ chế đồng bộ hóa cung cấp quyền truy cập độc quyền vào các phần quan trọng của mã trong các ứng dụng đa luồng. Theo Oracle, 2024, giao diện Lock cung cấp khả năng kiểm soát đồng bộ hóa linh hoạt hơn so với các khối synchronized truyền thống, bao gồm các nỗ lực giành quyền với thời gian chờ và hỗ trợ nhiều hàng đợi chờ.

Những điểm chính

  • Lock là giao diện quản lý khóa tường minh trong Java.
  • ReentrantLock là triển khai cơ bản hỗ trợ giành lại quyền bởi cùng một luồng.
  • ReadWriteLock tách khóa đọc và khóa ghi để cải thiện hiệu suất.
  • Deadlock là rủi ro chính khi sử dụng nhiều khóa đồng thời.
  • Không giống synchronized, Lock hỗ trợ thời gian chờ và chờ có thể ngắt.

Lock là gì?

Lock là giao diện từ gói java.util.concurrent.locks cung cấp các thao tác khóa và mở khóa tường minh để đồng bộ hóa truy cập dữ liệu. Không giống synchronized, Lock cho nhà phát triển toàn quyền kiểm soát cơ chế khóa.

Định nghĩa và vai trò trong đồng bộ hóa

Giao diện Lock được giới thiệu trong Java 5 như một giải pháp thay thế cho cơ chế synchronized tích hợp sẵn. Các phương thức chính là lock, unlock, tryLock và lockInterruptibly. Khóa giúp tổ chức truy cập dữ liệu an toàn trong môi trường đa luồng, ngăn chặn điều kiện cạnh tranh và hỏng dữ liệu.

Lợi thế chính của Lock so với synchronized là tính linh hoạt. Nhà phát triển có thể thử giành khóa với thời gian chờ, kiểm tra tính khả dụng mà không bị chặn hoặc tổ chức nhiều hàng đợi chờ với các ưu tiên khác nhau.

Lịch sử và phát triển

Trước khi giao diện Lock xuất hiện trong Java 5, phương pháp đồng bộ hóa duy nhất là synchronized, vốn có nhiều hạn chế: không có thời gian chờ, không có chờ có thể ngắt và chỉ một hàng đợi. Doug Lea đã thiết kế gói java.util.concurrent, bao gồm Lock như một khối xây dựng cơ bản.

Khóa hoạt động như thế nào?

Khóa quản lý truy cập thông qua cờ trạng thái nội bộ và hàng đợi chờ. Khi một luồng gọi lock(), cơ chế kiểm tra xem khóa có trống không và hoặc giành quyền hoặc đưa luồng vào hàng đợi cho đến khi được giải phóng.

Giành quyền và giải phóng nguyên tử

Cốt lõi của bất kỳ khóa nào là thao tác so sánh và hoán đổi (CAS) nguyên tử. Khi lock() được gọi, luồng cố gắng đặt cờ bận một cách nguyên tử. Nếu cờ đã được đặt, luồng bị chặn. Khi unlock(), cờ được xóa và một luồng đang chờ được đánh thức.

kotlin
import java.util.concurrent.locks.ReentrantLock

val lock = ReentrantLock()

fun performTask() {
    lock.lock()
    try {
        // phần quan trọng
        println("Luồng ${Thread.currentThread().name} đang làm việc")
    } finally {
        lock.unlock()
    }
}

Hàng đợi chờ và đánh thức

ReentrantLock sử dụng nội bộ danh sách liên kết đôi (hàng đợi khóa CLH) nơi mỗi luồng chờ được biểu diễn bằng một nút. Khi khóa được giải phóng, nút đầu của hàng đợi được đánh thức. Chế độ công bằng (fair) đảm bảo thứ tự FIFO, trong khi chế độ không công bằng cho phép một luồng mới giành khóa trước các luồng đang chờ để cải thiện thông lượng.

Các loại khóa chính

Trong hệ sinh thái Java hiện đại, có nhiều triển khai khóa khác nhau, mỗi loại được tối ưu hóa cho các kịch bản cụ thể. Chọn đúng khóa ảnh hưởng trực tiếp đến hiệu suất và độ tin cậy của ứng dụng đa luồng.

ReentrantLock

ReentrantLock là triển khai Lock cơ bản và được sử dụng nhiều nhất. Nó hỗ trợ giành lại quyền bởi cùng một luồng: nếu một luồng đã giữ khóa, gọi lock() lại không chặn nó. Điều này ngăn deadlock trong các lệnh gọi đệ quy.

ReentrantReadWriteLock

ReadWriteLock tách khóa thành hai chế độ: đọc và ghi. Nhiều luồng có thể đồng thời giữ khóa đọc, nhưng ghi yêu cầu quyền truy cập độc quyền. Điều này cải thiện đáng kể hiệu suất khi đọc thường xuyên và ghi hiếm.

StampedLock

StampedLock là triển khai mới nhất, được giới thiệu trong Java 8. Nó hỗ trợ ba chế độ: ghi, đọc và đọc lạc quan. Đọc lạc quan không chặn các luồng khác và xác thực dữ liệu sau khi đọc, mang lại hiệu suất tăng 10-20% so với ReadWriteLock.

KhóaPhiên bản JavaChế độHiệu suất
ReentrantLockJava 5độc quyềncao
ReadWriteLockJava 5đọc + ghitrung bình
StampedLockJava 8đọc + ghi + lạc quanrất cao

ReentrantLock và các đặc điểm

ReentrantLock là triển khai Lock phổ biến nhất, cung cấp một số tính năng không có trong synchronized. Hiểu các đặc điểm của nó là cần thiết để làm việc hiệu quả với đa luồng.

Tính công bằng của khóa (fairness)

Hàm tạo của ReentrantLock chấp nhận tham số fair. Khi true, khóa đảm bảo thứ tự FIFO; khi false, một luồng mới có thể giành khóa trước các luồng đang chờ. Chế độ công bằng ngăn chặn tình trạng đói nhưng giảm thông lượng 10-20% do chi phí duy trì hàng đợi.

Thời gian chờ và chờ có thể ngắt

Không giống synchronized, ReentrantLock hỗ trợ tryLock với thời gian chờ. Nếu không thể giành khóa trong thời gian chỉ định, luồng tiếp tục thực thi thay vì bị chặn vô thời hạn. Phương thức lockInterruptibly cho phép ngắt một luồng đang chờ qua Thread.interrupt().

kotlin
val lock = ReentrantLock()

fun tryTask() {
    if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
        try {
            println("Đã giành được khóa")
        } finally {
            lock.unlock()
        }
    } else {
        println("Không thể giành được khóa")
    }
}

Điều kiện (Conditions)

ReentrantLock hỗ trợ nhiều biến điều kiện thông qua phương thức newCondition(). Mỗi Condition có hàng đợi chờ riêng, cho phép các kịch bản đánh thức phức tạp. Các phương thức await() và signal() đã thay thế wait() và notify() từ các khối synchronized, nhưng với hỗ trợ nhiều hàng đợi.

ReadWriteLock và StampedLock

ReadWriteLock và StampedLock giải quyết vấn đề tối ưu hóa truy cập khi đọc chiếm ưu thế hơn ghi. Chúng hiệu quả hơn đáng kể so với ReentrantLock trong các kịch bản mà đọc xảy ra thường xuyên hơn ghi.

ReadWriteLock trong thực tế

Giao diện ReadWriteLock chứa hai phương thức: readLock() và writeLock(). Khóa đọc có thể được giữ bởi nhiều luồng đồng thời, trong khi khóa ghi là độc quyền. Một ví dụ điển hình là bộ đệm an toàn luồng: nhiều luồng đọc dữ liệu trong khi chỉ một luồng định kỳ cập nhật chúng.

kotlin
class SafeCache<K, V> {
    private val map = mutableMapOf<K, V>()
    private val rwLock = ReentrantReadWriteLock()

    fun get(key: K): V? {
        rwLock.readLock().lock()
        return try { map[key] } finally { rwLock.readLock().unlock() }
    }

    fun put(key: K, value: V) {
        rwLock.writeLock().lock()
        return try { map[key] = value } finally { rwLock.writeLock().unlock() }
    }
}

StampedLock và đọc lạc quan

StampedLock thêm chế độ thứ ba — tryOptimisticRead. Chế độ này không chặn các luồng khác mà chỉ ghi lại dấu (stamp) trạng thái. Sau khi đọc, nhà phát triển gọi validate(stamp) để kiểm tra xem dữ liệu có thay đổi trong khi đọc không. Nếu dữ liệu đã thay đổi, thao tác phải được thực hiện lại.

Khóa trong phát triển di động

Trong các ứng dụng di động, khóa được sử dụng để điều phối quyền truy cập vào dữ liệu chia sẻ giữa các luồng. Tuy nhiên, việc sử dụng chúng đòi hỏi sự thận trọng đặc biệt do tài nguyên thiết bị hạn chế và nhu cầu duy trì khả năng phản hồi của giao diện.

Khóa trong Android (Kotlin)

Trên Android, ReentrantLock hữu ích khi làm việc với Room, bộ đệm và tệp. Điều quan trọng cần nhớ: không bao giờ giành khóa trên luồng chính. Đối với mã bất đồng bộ, coroutines và Mutex từ kotlinx.coroutines được ưa chuộng hơn vì chúng tạm dừng coroutine thay vì chặn luồng.

Khóa trong iOS (Swift)

Trong iOS, NSLock tiêu chuẩn ít được sử dụng hơn — các nhà phát triển thích DispatchQueue với cờ barrier hoặc os_unfair_lock. Swift 5.7+ cung cấp các cơ chế đồng bộ hóa hiện đại thông qua actor, tự động bảo vệ trạng thái.

swift
import Foundation

actor DataStore {
    private var items: [String] = []

    func add(_ item: String) {
        items.append(item)
    }

    func getAll() -> [String] {
        items
    }
}

Khuyến nghị tránh deadlock

Để tránh deadlock, hãy tuân theo thứ tự giành khóa nhất quán trong toàn bộ dự án. Sử dụng tryLock với thời gian chờ thay vì lock() ở bất cứ nơi nào có thể xảy ra chặn kéo dài. Cân nhắc sử dụng thuật toán Lock-Free (AtomicReference, ConcurrentHashMap) thay vì khóa truyền thống.

Thực hành tốt nhất khi làm việc với Lock

Sử dụng Lock đòi hỏi kỷ luật và tuân thủ một số quy tắc giúp ngăn chặn deadlock và suy giảm hiệu suất. Những thực hành này đã được cộng đồng Java phát triển qua 20 năm sử dụng gói java.util.concurrent.

Giải phóng trong finally

Mẫu quan trọng nhất là lock trong finally. Bất kể phần quan trọng hoàn thành thành công hay ném ngoại lệ, khóa phải được giải phóng. Điều này đảm bảo các luồng khác không bị chặn vĩnh viễn do một lỗi duy nhất. Trong Kotlin, mẫu này được giải quyết một cách thanh lịch thông qua phần mở rộng withLock.

Giảm thiểu thời gian giữ khóa

Phần quan trọng nên càng ngắn càng tốt. Không bao giờ thực hiện I/O, yêu cầu mạng hoặc tính toán kéo dài bên trong khóa. Nếu bạn cần đọc dữ liệu từ máy chủ, hãy lấy dữ liệu trước, sau đó chỉ giành khóa để cập nhật trạng thái chia sẻ. Điều này giảm tranh chấp và cải thiện thông lượng hệ thống.

Thứ tự giành khóa nhất quán

Để ngăn chặn deadlock khi làm việc với nhiều khóa, hãy thiết lập thứ tự giành khóa toàn cục trong toàn bộ dự án. Nếu lockA được giành trước, sau đó lockB — bất kỳ trình tự ngược lại nào phải bị cấm bởi các quy tắc xem xét mã. Sử dụng các trình phân tích tĩnh như SpotBugs và IntelliJ Inspections để xác minh tự động.

Câu hỏi thường gặp

Sự khác biệt giữa Lock và synchronized là gì?

Lock là giao diện tường minh với hỗ trợ thời gian chờ và chờ có thể ngắt. synchronized tự động giành và giải phóng monitor, nhưng không cho phép sử dụng tryLock, lockInterruptibly hoặc nhiều Conditions. Lock linh hoạt hơn nhưng yêu cầu giải phóng thủ công trong finally.

Khóa công bằng (fair lock) là gì?

Khóa công bằng đảm bảo thứ tự FIFO: luồng chờ lâu nhất nhận được khóa trước. Khóa không công bằng có thể cấp quyền truy cập cho luồng mới trước các luồng đang chờ, điều này tăng thông lượng nhưng có thể gây ra tình trạng đói cho các luồng đang chờ.

Làm thế nào để tránh deadlock khi sử dụng Lock?

Tuân theo thứ tự cố định để giành tất cả các khóa, sử dụng tryLock với thời gian chờ thay vì lock() vô điều kiện và giảm thiểu số lượng khóa được giữ đồng thời. Sử dụng cấu trúc dữ liệu Lock-Free cũng làm giảm nguy cơ deadlock.

Condition trong Lock là gì?

Condition là phiên bản tương tự wait/notify cho Lock, cho phép nhiều hàng đợi chờ độc lập. Mỗi lệnh gọi newCondition() tạo một hàng đợi riêng biệt, cung cấp khả năng kiểm soát đánh thức luồng chính xác hơn so với hàng đợi đơn của synchronized.

Nên chọn Lock nào cho ứng dụng di động?

Đối với Android với coroutines, sử dụng Mutex từ kotlinx.coroutines — nó tạm dừng coroutine thay vì chặn luồng. Đối với iOS với Swift 5.7+, actor được ưa chuộng hơn vì chúng tự động đồng bộ hóa quyền truy cập trạng thái. Dành ReentrantLock cho mã kế thừa và các kịch bản cấp thấp.

Tóm tắt

  • Lock là giao diện quản lý khóa tường minh từ java.util.concurrent.locks.
  • ReentrantLock là triển khai chính hỗ trợ giành lại và công bằng.
  • ReadWriteLock tách khóa đọc và ghi cho các kịch bản đọc nhiều.
  • StampedLock thêm đọc lạc quan để đạt hiệu suất tối đa.
  • Thời gian chờ và Conditions là lợi thế chính của Lock so với synchronized.
  • Deadlock được ngăn chặn bằng thứ tự giành khóa nhất quán và sử dụng tryLock.
  • Trong phát triển di động, coroutines (Android) và actor (iOS) được khuyến nghị.

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.

Thảo luận dự án

Đọc thêm