So sánh và Ứng dụng Synchronized cùng Lock trong Java

Trong lập trình đa luồng Java, việc đảm bảo tính toàn vẹn dữ liệu khi nhiều luồng cùng truy cập một tài nguyên chia sẻ là vô cùng quan trọng. Java cung cấp hai cơ chế chính để đạt được điều này: từ khóa synchronized và interface Lock. Bài viết này sẽ đi sâu vào cách thức hoạt động, ứng dụng và sự khác biệt giữa hai cơ chế này.

1. Giới thiệu về Synchronized

synchronized là một từ khóa cơ bản trong Java, được tích hợp sẵn để quản lý quyền truy cập của các luồng vào các khối mã hoặc phương thức quan trọng, ngăn chặn tình trạng race condition (điều kiện tranh chấp) và đảm bảo tính đồng bộ.

1.1. Các dạng áp dụng của từ khóa synchronized

Từ khóa synchronized có thể được áp dụng theo nhiều cách khác nhau, mỗi cách có phạm vi khóa riêng:

  • Khóa một khối mã: Khi áp dụng cho một khối mã (ví dụ: synchronized (object) { ... }), nó khóa đối tượng được chỉ định trong ngoặc đơn. Chỉ một luồng được phép thực thi khối mã bên trong của đối tượng đó tại một thời điểm.
  • Khóa một phương thức thực thể: Khi khai báo trên một phương thức không tĩnh (ví dụ: public synchronized void myMethod()), nó khóa đối tượng hiện tại (this) của lớp. Điều này có nghĩa là chỉ một luồng có thể gọi bất kỳ phương thức synchronized nào của cùng một đối tượng tại một thời điểm.
  • Khóa một phương thức tĩnh: Khi áp dụng cho một phương thức tĩnh (ví dụ: public static synchronized void staticMethod()), nó khóa đối tượng Class của lớp đó. Điều này đảm bảo rằng chỉ một luồng duy nhất có thể thực thi bất kỳ phương thức tĩnh synchronized nào của lớp đó tại một thời điểm, trên tất cả các thể hiện của lớp.

Một điểm cần lưu ý là synchronized không phải là một phần của chữ ký phương thức và do đó không được kế thừa. Nếu một phương thức synchronized ở lớp cha được ghi đè ở lớp con, phương thức ở lớp con sẽ không tự động đồng bộ hóa. Bạn phải thêm từ khóa synchronized một cách tường minh vào phương thức của lớp con nếu muốn duy trì tính đồng bộ.

1.2. Ví dụ minh họa với synchronized: Hệ thống bán hàng

Hãy xem xét một hệ thống bán hàng đơn giản, nơi nhiều người bán hàng cùng lúc cố gắng bán sản phẩm từ một kho hàng chung. Nếu không có cơ chế đồng bộ hóa, có thể xảy ra tình trạng bán quá số lượng sản phẩm hoặc số liệu tồn kho bị sai lệch.

package com.example.concurrency.sync;

class ProductInventory {
    private int stockCount = 30; // Số lượng sản phẩm có sẵn trong kho

    public synchronized void sellProduct() {
        if (stockCount > 0) {
            System.out.println(Thread.currentThread().getName() +
                               ": Đã bán một sản phẩm. Số lượng còn lại: " + (--stockCount));
        } else {
            System.out.println(Thread.currentThread().getName() + ": Hết hàng.");
        }
    }
}

public class StoreSales {
    public static void main(String[] args) {
        ProductInventory inventory = new ProductInventory();

        // Tạo ba luồng đại diện cho ba người bán hàng
        new Thread(() -> {
            for (int i = 0; i < 40; i++) { // Mỗi người bán thử bán 40 lần
                inventory.sellProduct();
            }
        }, "SellerA").start();

        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                inventory.sellProduct();
            }
        }, "SellerB").start();

        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                inventory.sellProduct();
            }
        }, "SellerC").start();
    }
}

Trong ví dụ trên, phương thức sellProduct() được đánh dấu synchronized, đảm bảo rằng chỉ có một luồng (người bán) được phép thực thi khối mã bán hàng tại một thời điểm, tránh các vấn đề về đồng bộ hóa.

2. Khám phá Interface Lock

Interface Lock, được giới thiệu trong Java 5 cùng với gói java.util.concurrent.locks, cung cấp một phương pháp kiểm soát việc khóa linh hoạt và mạnh mẽ hơn nhiều so với từ khóa synchronized. Nó mang đến cho lập trình viên khả năng kiểm soát chi tiết hơn đối với các thao tác khóa, bao gồm cả việc thử khóa không chặn, khóa có thời gian chờ, và khả năng phản hồi ngắt.

2.1. Điểm khác biệt cơ bản giữa Locksynchronized

  • Nguồn gốc: synchronized là một từ khóa được tích hợp sẵn trong ngôn ngữ Java, do JVM quản lý hoàn toàn. Ngược lại, Lock là một interface và được triển khai thông qua các lớp cụ thể như ReentrantLock, mang lại khả năng kiểm soát khóa ở cấp độ lập trình.
  • Giải phóng khóa: synchronized tự động giải phóng khóa khi khối mã hoặc phương thức hoàn tất thực thi (bình thường hoặc do ngoại lệ). Với Lock, bạn phải tự tay gọi phương thức unlock() để giải phóng khóa. Việc quên giải phóng khóa có thể dẫn đến tình trạng deadlock (tắc nghẽn) nghiêm trọng, khiến các luồng khác bị chặn vĩnh viễn. Do đó, việc sử dụng khối try-finally là bắt buộc khi làm việc với Lock để đảm bảo khóa luôn được giải phóng.

2.2. Ví dụ minh họa với Lock: Quản lý kho hàng

Chúng ta sẽ tái hiện ví dụ về hệ thống bán hàng, nhưng lần này sử dụng ReentrantLock để quản lý truy cập kho hàng.

package com.example.concurrency.lock;

import java.util.concurrent.locks.ReentrantLock;

class WarehouseInventory {
    private int itemCount = 30; // Tổng số mặt hàng trong kho
    private final ReentrantLock inventoryLock = new ReentrantLock(); // Khóa để bảo vệ truy cập

    public void reduceItemCount() {
        inventoryLock.lock(); // Yêu cầu khóa
        try {
            if (itemCount > 0) {
                System.out.println(Thread.currentThread().getName() +
                                   ": Đã xuất kho một mặt hàng. Số lượng còn lại: " + (--itemCount));
            } else {
                System.out.println(Thread.currentThread().getName() + ": Kho đã hết hàng.");
            }
        } finally {
            inventoryLock.unlock(); // Đảm bảo giải phóng khóa ngay cả khi có lỗi
        }
    }
}

public class WarehouseOperations {
    public static void main(String[] args) {
        WarehouseInventory inventory = new WarehouseInventory();

        // Tạo các luồng mô phỏng công nhân xuất hàng
        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                inventory.reduceItemCount();
            }
        }, "WorkerX").start();

        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                inventory.reduceItemCount();
            }
        }, "WorkerY").start();

        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                inventory.reduceItemCount();
            }
        }, "WorkerZ").start();
    }
}

Trong ví dụ này, phương thức reduceItemCount() sử dụng một đối tượng ReentrantLock. Khóa được lấy bằng lock() và đảm bảo được giải phóng trong khối finally bằng unlock().

2.3. So sánh chi tiết Locksynchronized

Ngoài những điểm khác biệt cơ bản đã nêu, Locksynchronized còn có những điểm khác biệt sâu hơn:

  1. Bản chất và quản lý:
    • synchronized là một từ khóa được JVM quản lý trực tiếp, đơn giản và dễ sử dụng.
    • Lock là một interface với các lớp triển khai cụ thể, cung cấp khả năng kiểm soát lập trình cao hơn nhưng đòi hỏi quản lý thủ công.
  2. Xử lý ngoại lệ và giải phóng khóa:
    • synchronized: JVM tự động giải phóng khóa khi khối mã kết thúc, dù là bình thường hay do ngoại lệ, giúp ngăn chặn deadlock.
    • Lock: Yêu cầu lập trình viên phải gọi unlock() một cách tường minh, thường là trong khối finally, để đảm bảo khóa được giải phóng. Nếu không, có thể dẫn đến deadlock.
  3. Khả năng phản hồi ngắt:
    • synchronized: Một luồng đang chờ khóa synchronized sẽ tiếp tục chờ cho đến khi có thể lấy được khóa mà không phản ứng với các tín hiệu ngắt (interrupt).
    • Lock: Cung cấp phương thức lockInterruptibly(), cho phép một luồng đang chờ khóa có thể bị ngắt và thoát khỏi trạng thái chờ.
  4. Thăm dò khóa và thời gian chờ:
    • synchronized: Không có cơ chế nào để kiểm tra xem khóa có sẵn hay không, hoặc để thử lấy khóa với một thời gian chờ giới hạn. Luồng sẽ chặn vô thời hạn.
    • Lock: Cung cấp các phương thức như tryLock() (thử lấy khóa không chặn, trả về true nếu thành công) và tryLock(long timeout, TimeUnit unit) (thử lấy khóa với thời gian chờ), cho phép lập trình viên kiểm soát cách luồng tương tác với khóa.
  5. Hiệu suất đọc và linh hoạt:
    • synchronized: Chỉ cung cấp một loại khóa độc quyền, nghĩa là mọi truy cập (đọc hoặc ghi) đều phải đợi.
    • Lock: Với các triển khai như ReentrantReadWriteLock, nó có thể cung cấp hiệu suất tốt hơn đáng kể cho các hoạt động đọc đồng thời, cho phép nhiều luồng đọc cùng lúc nhưng chỉ một luồng ghi tại một thời điểm.
  6. Hiệu suất dưới tải nặng:
    • Trong các tình huống cạnh tranh tài nguyên thấp, hiệu suất của synchronizedLock thường tương đương.
    • Tuy nhiên, khi cạnh tranh cao (nhiều luồng tranh giành cùng một tài nguyên), Lock thường thể hiện hiệu suất vượt trội hơn nhờ khả năng điều khiển chi tiết và các tối ưu hóa nội bộ của nó.

Thẻ: Java Concurrency multithreading synchronized Lock

Đăng vào ngày 15 tháng 7 lúc 19:34