3. Java Locks

Các Cơ chế Khóa (Locks) trong Java

Phần này trình bày các cơ chế khóa trong gói java.util.concurrent.locks, bao gồm Explicit Locks, ReadWriteLock, Condition, Lock Fairness, và TryLock. Nội dung được giải thích chi tiết, kèm ví dụ minh họa để hỗ trợ việc học chuyên sâu về concurrency trong Java.


1. Explicit Locks

1.1. Sử dụng java.util.concurrent.locks.LockReentrantLock

Gói java.util.concurrent.locks cung cấp giao diện Lock và lớp triển khai phổ biến ReentrantLock, cho phép kiểm soát khóa linh hoạt hơn so với từ khóa synchronized.

  • Lock: Giao diện định nghĩa các phương thức cơ bản như lock(), unlock(), tryLock(), và newCondition().

  • ReentrantLock: Một triển khai của Lock, hỗ trợ reentrant locking (thread có thể lấy lại khóa mà nó đã giữ).

Ví dụ ReentrantLock:

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();
    private int counter = 0;

    public void increment() {
        lock.lock();
        try {
            counter++;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockExample example = new ReentrantLockExample();
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Counter: " + example.counter); // Kết quả: 2000
    }
}
  • Giải thích: lock() lấy khóa, unlock() trả khóa. Khối try-finally đảm bảo khóa luôn được trả, ngay cả khi có ngoại lệ.

1.2. So sánh với synchronized

Tiêu chí

synchronized

ReentrantLock

Cơ chế khóa

Sử dụng khóa nội tại của đối tượng.

Khóa rõ ràng, quản lý bởi Lock instance.

Tính linh hoạt

Ít linh hoạt, chỉ hỗ trợ khóa cơ bản.

Hỗ trợ tryLock, fairness, Condition.

Hiệu suất

Có thể gây tranh chấp khóa cao.

Linh hoạt hơn, nhưng cần quản lý thủ công.

Xử lý ngoại lệ

Tự động trả khóa khi thoát khối.

Yêu cầu unlock trong finally.

Timeout

Không hỗ trợ chờ khóa có thời hạn.

Hỗ trợ tryLock(timeout).

Fairness

Không đảm bảo công bằng (thread starvation).

Hỗ trợ chế độ công bằng (fair mode).

Khi nào dùng gì?

  • Dùng synchronized cho các trường hợp đơn giản, cần ít mã.

  • Dùng ReentrantLock khi cần tính năng nâng cao như timeout, fairness, hoặc điều phối luồng phức tạp.


2. ReadWriteLock

2.1. Khóa Đọc-Ghi (ReentrantReadWriteLock)

ReadWriteLock là một giao diện cung cấp hai loại khóa:

  • Read Lock: Cho phép nhiều thread đọc cùng lúc (shared lock).

  • Write Lock: Chỉ cho phép một thread ghi tại một thời điểm (exclusive lock).

Lớp ReentrantReadWriteLock là triển khai phổ biến, tối ưu hóa hiệu suất khi có nhiều thao tác đọc hơn ghi.

Ví dụ ReentrantReadWriteLock:

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private int data = 0;

    public void read() {
        rwLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " read: " + data);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            rwLock.readLock().unlock();
        }
    }

    public void write(int value) {
        rwLock.writeLock().lock();
        try {
            data = value;
            System.out.println(Thread.currentThread().getName() + " wrote: " + data);
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    public static void main(String[] args) {
        ReadWriteLockExample example = new ReadWriteLockExample();
        Runnable readTask = example::read;
        Runnable writeTask = () -> example.write(42);

        Thread t1 = new Thread(readTask, "Reader1");
        Thread t2 = new Thread(readTask, "Reader2");
        Thread t3 = new Thread(writeTask, "Writer");
        t1.start();
        t2.start();
        t3.start();
    }
}
  • Kết quả: Nhiều thread đọc có thể chạy đồng thời, nhưng thread ghi phải chờ đến khi không có thread đọc hoặc ghi nào giữ khóa.

2.2. Ứng dụng

  • Cơ sở dữ liệu cache: Nhiều thread đọc dữ liệu cache, nhưng ít thread cập nhật.

  • Hệ thống file: Nhiều thread đọc file, chỉ một thread ghi file.

  • Web server: Nhiều request đọc dữ liệu tĩnh, ít request cập nhật.

2.3. Lợi ích

  • Tăng hiệu suất khi số lượng đọc lớn hơn ghi.

  • Đảm bảo thread safety mà không cần khóa toàn bộ như synchronized.


3. Condition

3.1. Sử dụng Condition để Điều phối Luồng

Condition là một cơ chế điều phối luồng, tương tự wait()notify() của synchronized, nhưng linh hoạt hơn. Nó được tạo từ một Lock (thường là ReentrantLock) thông qua phương thức newCondition().

  • Phương thức chính:

    • await(): Thread chờ và trả khóa, tương tự wait().

    • signal(): Đánh thức một thread đang chờ, tương tự notify().

    • signalAll(): Đánh thức tất cả thread đang chờ, tương tự notifyAll().

Ví dụ Condition (Producer-Consumer):

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionExample {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    private final Queue<Integer> queue = new LinkedList<>();
    private final int capacity = 5;

    public void produce(int item) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == capacity) {
                notFull.await(); // Chờ nếu queue đầy
            }
            queue.add(item);
            System.out.println("Produced: " + item);
            notEmpty.signal(); // Báo hiệu queue không rỗng
        } finally {
            lock.unlock();
        }
    }

    public int consume() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                notEmpty.await(); // Chờ nếu queue rỗng
            }
            int item = queue.poll();
            System.out.println("Consumed: " + item);
            notFull.signal(); // Báo hiệu queue không đầy
            return item;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ConditionExample example = new ConditionExample();
        Runnable producer = () -> {
            try {
                for (int i = 0; i < 10; i++) {
                    example.produce(i);
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        Runnable consumer = () -> {
            try {
                for (int i = 0; i < 10; i++) {
                    example.consume();
                    Thread.sleep(200);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        new Thread(producer).start();
        new Thread(consumer).start();
    }
}
  • Giải thích: Condition được dùng để điều phối giữa producer và consumer, đảm bảo queue không bị đầy hoặc rỗng.

3.2. So sánh với wait()notify()

  • Linh hoạt hơn: Một Lock có thể có nhiều Condition để điều phối các nhóm thread khác nhau.

  • Hỗ trợ timeout: await(long time, TimeUnit unit) cho phép chờ có thời hạn.

  • Không yêu cầu synchronized: Hoạt động với Lock thay vì khóa nội tại.


4. Lock Fairness

4.1. Đảm bảo Công bằng trong Việc Cấp Khóa

  • Fairness (công bằng) đảm bảo rằng các thread chờ khóa được cấp khóa theo thứ tự chúng yêu cầu (FIFO - First In, First Out).

  • ReentrantLock hỗ trợ chế độ công bằng thông qua constructor:

    • ReentrantLock(boolean fair): Nếu fair = true, khóa được cấp công bằng.

Ví dụ Lock Fairness:

import java.util.concurrent.locks.ReentrantLock;

public class LockFairnessExample {
    private final ReentrantLock lock;

    public LockFairnessExample(boolean fair) {
        this.lock = new ReentrantLock(fair);
    }

    public void accessResource() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " accessed resource");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        LockFairnessExample example = new LockFairnessExample(true); // Fair lock
        Runnable task = () -> {
            for (int i = 0; i < 3; i++) {
                example.accessResource();
            }
        };

        Thread t1 = new Thread(task, "Thread-1");
        Thread t2 = new Thread(task, "Thread-2");
        t1.start();
        t2.start();
    }
}
  • Kết quả: Với fair = true, các thread lần lượt truy cập tài nguyên theo thứ tự yêu cầu.

4.2. Ưu và Nhược điểm

  • Ưu điểm:

    • Giảm nguy cơ starvation (thread bị bỏ đói).

    • Đảm bảo tính công bằng trong môi trường nhiều thread.

  • Nhược điểm:

    • Hiệu suất thấp hơn do phải duy trì hàng đợi thread.

    • Không cần thiết trong các ứng dụng không yêu cầu thứ tự nghiêm ngặt.


5. TryLock và Timeout

5.1. Sử dụng tryLock để Tránh Deadlock

  • Phương thức tryLock() cho phép thread cố gắng lấy khóa mà không bị chặn vô thời hạn:

    • tryLock(): Trả về true nếu lấy được khóa, false nếu không.

    • tryLock(long time, TimeUnit unit): Thử lấy khóa trong khoảng thời gian xác định.

  • tryLock giúp tránh deadlock bằng cách từ bỏ nếu không lấy được khóa.

Ví dụ TryLock:

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class TryLockExample {
    private final ReentrantLock lock1 = new ReentrantLock();
    private final ReentrantLock lock2 = new ReentrantLock();

    public void process1() {
        if (lock1.tryLock()) {
            try {
                System.out.println("Process1 acquired lock1");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (lock2.tryLock()) {
                    try {
                        System.out.println("Process1 acquired lock2");
                    } finally {
                        lock2.unlock();
                    }
                } else {
                    System.out.println("Process1 failed to acquire lock2");
                }
            } finally {
                lock1.unlock();
            }
        }
    }

    public void process2() {
        if (lock2.tryLock()) {
            try {
                System.out.println("Process2 acquired lock2");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (lock1.tryLock(1, TimeUnit.SECONDS)) {
                    try {
                        System.out.println("Process2 acquired lock1");
                    } finally {
                        lock1.unlock();
                    }
                } else {
                    System.out.println("Process2 failed to acquire lock1");
                }
            } finally {
                lock2.unlock();
            }
        }
    }

    public static void main(String[] args) {
        TryLockExample example = new TryLockExample();
        new Thread(example::process1, "Thread-1").start();
        new Thread(example::process2, "Thread-2").start();
    }
}
  • Giải thích: tryLock ngăn thread chờ vô hạn, giảm nguy cơ deadlock khi không lấy được cả hai khóa.

5.2. Ứng dụng

  • Tránh deadlock: Thử lấy khóa trong các tình huống có nguy cơ khóa chéo.

  • Hiệu suất: Giảm thời gian chờ trong các ứng dụng thời gian thực.

  • Kiểm soát luồng: Cho phép thread thực hiện hành động thay thế nếu không lấy được khóa.


Kết luận

Các cơ chế khóa trong java.util.concurrent.locks cung cấp sự linh hoạt và hiệu quả cao hơn so với synchronized:

  • ReentrantLock mang lại các tính năng như timeout, fairness, và điều phối luồng.

  • ReadWriteLock tối ưu hóa hiệu suất cho các tình huống nhiều đọc, ít ghi.

  • Condition cung cấp cách điều phối luồng linh hoạt hơn wait()/notify().

  • Lock Fairness đảm bảo công bằng, nhưng cần cân nhắc về hiệu suất.

  • TryLock giúp tránh deadlock và cải thiện khả năng đáp ứng.

Phần tiếp theo có thể tập trung vào Executor Framework để quản lý thread hiệu quả hơn. Nếu bạn muốn, tôi có thể tiếp tục với nội dung này hoặc cung cấp thêm ví dụ thực hành!

Last updated