第5章

🔐 Lock接口

深入理解Java并发编程中的高级锁机制,掌握Lock接口的设计思想和实际应用

学习目标

Lock接口详解

Lock接口是Java并发包(java.util.concurrent.locks)中的核心接口,它提供了比synchronized关键字更灵活、更强大的锁机制。Lock接口的设计目标是提供更细粒度的锁控制,支持可中断的锁获取、超时锁获取以及非阻塞的锁获取。

核心理解

Lock接口不是用来替代synchronized的,而是在synchronized无法满足需求时提供更多选择。它们各有适用场景。

Lock接口的核心方法

public interface Lock {
    // 获取锁,如果锁不可用则阻塞等待
    void lock();
    
    // 可中断的锁获取
    void lockInterruptibly() throws InterruptedException;
    
    // 尝试获取锁,立即返回结果
    boolean tryLock();
    
    // 在指定时间内尝试获取锁
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    
    // 释放锁
    void unlock();
    
    // 创建条件变量
    Condition newCondition();
}

Lock vs synchronized

特性 synchronized Lock
使用方式 关键字,自动获取和释放 接口,手动获取和释放
可中断性 不可中断 支持可中断锁
超时机制 不支持 支持超时获取锁
公平性 非公平锁 支持公平锁和非公平锁
条件变量 单一条件(wait/notify) 支持多个条件变量
性能 JVM优化,轻量级场景更好 高竞争场景性能更好

ReentrantLock详解

ReentrantLock是Lock接口最常用的实现类,它是一个可重入的互斥锁。"可重入"意味着同一个线程可以多次获取同一把锁,这与synchronized的行为一致。

基本使用方法

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;
    
    public void increment() {
        lock.lock();  // 获取锁
        try {
            count++;  // 临界区代码
            System.out.println("Count: " + count);
        } finally {
            lock.unlock();  // 释放锁(必须在finally块中)
        }
    }
    
    public void safeMethod() {
        lock.lock();
        try {
            // 可重入性:同一线程可以再次获取锁
            increment();
        } finally {
            lock.unlock();
        }
    }
}
重要提醒

使用Lock时必须在finally块中释放锁,否则可能导致死锁。这是与synchronized的重要区别。

可重入性演示

public class ReentrantDemo {
    private final ReentrantLock lock = new ReentrantLock();
    
    public void methodA() {
        lock.lock();
        try {
            System.out.println("Method A - Hold count: " + lock.getHoldCount());
            methodB();  // 调用另一个需要锁的方法
        } finally {
            lock.unlock();
        }
    }
    
    public void methodB() {
        lock.lock();
        try {
            System.out.println("Method B - Hold count: " + lock.getHoldCount());
            methodC();
        } finally {
            lock.unlock();
        }
    }
    
    public void methodC() {
        lock.lock();
        try {
            System.out.println("Method C - Hold count: " + lock.getHoldCount());
        } finally {
            lock.unlock();
        }
    }
}

公平锁和非公平锁

ReentrantLock支持两种锁获取方式:公平锁和非公平锁。这是通过构造函数参数来控制的。

公平锁(Fair Lock)
按照线程请求锁的顺序来分配锁,先到先得。保证了线程获取锁的公平性,但性能相对较低。
非公平锁(Unfair Lock)
不保证线程获取锁的顺序,新来的线程可能比等待中的线程更早获得锁。性能更好,但可能导致线程饥饿。

公平锁与非公平锁的使用

import java.util.concurrent.locks.ReentrantLock;

public class FairVsUnfairLock {
    // 非公平锁(默认)
    private final ReentrantLock unfairLock = new ReentrantLock();
    
    // 公平锁
    private final ReentrantLock fairLock = new ReentrantLock(true);
    
    public void unfairMethod() {
        unfairLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " acquired unfair lock");
            Thread.sleep(100);  // 模拟工作
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            unfairLock.unlock();
        }
    }
    
    public void fairMethod() {
        fairLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " acquired fair lock");
            Thread.sleep(100);  // 模拟工作
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            fairLock.unlock();
        }
    }
}

性能对比测试

public class LockPerformanceTest {
    private static final int THREAD_COUNT = 10;
    private static final int ITERATIONS = 100000;
    
    public static void main(String[] args) throws InterruptedException {
        testLockPerformance("非公平锁", new ReentrantLock(false));
        testLockPerformance("公平锁", new ReentrantLock(true));
    }
    
    private static void testLockPerformance(String lockType, ReentrantLock lock) 
            throws InterruptedException {
        long startTime = System.currentTimeMillis();
        CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
        
        for (int i = 0; i < THREAD_COUNT; i++) {
            new Thread(() -> {
                for (int j = 0; j < ITERATIONS; j++) {
                    lock.lock();
                    try {
                        // 模拟少量工作
                    } finally {
                        lock.unlock();
                    }
                }
                latch.countDown();
            }).start();
        }
        
        latch.await();
        long endTime = System.currentTimeMillis();
        System.out.println(lockType + "耗时: " + (endTime - startTime) + "ms");
    }
}

可中断锁

Lock接口提供了可中断的锁获取方法lockInterruptibly(),这允许等待锁的线程响应中断信号。这在需要取消长时间等待的场景中非常有用。

import java.util.concurrent.locks.ReentrantLock;

public class InterruptibleLockExample {
    private final ReentrantLock lock = new ReentrantLock();
    
    public void interruptibleMethod() {
        try {
            // 可中断的锁获取
            lock.lockInterruptibly();
            try {
                // 模拟长时间工作
                System.out.println(Thread.currentThread().getName() + " 开始工作");
                Thread.sleep(5000);
                System.out.println(Thread.currentThread().getName() + " 工作完成");
            } finally {
                lock.unlock();
            }
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + " 被中断");
            Thread.currentThread().interrupt();  // 恢复中断状态
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        InterruptibleLockExample example = new InterruptibleLockExample();
        
        // 启动第一个线程,获取锁并长时间持有
        Thread thread1 = new Thread(example::interruptibleMethod, "Thread-1");
        thread1.start();
        
        Thread.sleep(100);  // 确保thread1先获取锁
        
        // 启动第二个线程,尝试获取锁
        Thread thread2 = new Thread(example::interruptibleMethod, "Thread-2");
        thread2.start();
        
        Thread.sleep(2000);  // 等待2秒后中断thread2
        
        System.out.println("中断 Thread-2");
        thread2.interrupt();
        
        thread1.join();
        thread2.join();
    }
}
使用场景

可中断锁特别适用于需要响应用户取消操作的场景,比如GUI应用中的后台任务,或者需要超时控制的服务。

锁超时机制

tryLock()方法提供了非阻塞和超时的锁获取方式,这可以有效避免死锁,并提供更好的用户体验。

非阻塞锁获取

public class TryLockExample {
    private final ReentrantLock lock = new ReentrantLock();
    private int counter = 0;
    
    public void tryLockMethod() {
        if (lock.tryLock()) {  // 立即尝试获取锁
            try {
                counter++;
                System.out.println(Thread.currentThread().getName() + 
                    " 获取锁成功,counter = " + counter);
                Thread.sleep(1000);  // 模拟工作
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println(Thread.currentThread().getName() + 
                " 获取锁失败,执行其他逻辑");
            // 执行不需要锁的其他逻辑
            doAlternativeWork();
        }
    }
    
    private void doAlternativeWork() {
        System.out.println(Thread.currentThread().getName() + 
            " 执行不需要锁的工作");
    }
}

超时锁获取

import java.util.concurrent.TimeUnit;

public class TimeoutLockExample {
    private final ReentrantLock lock = new ReentrantLock();
    
    public void timeoutLockMethod() {
        try {
            // 尝试在3秒内获取锁
            if (lock.tryLock(3, TimeUnit.SECONDS)) {
                try {
                    System.out.println(Thread.currentThread().getName() + 
                        " 在超时时间内获取锁成功");
                    Thread.sleep(2000);  // 模拟工作
                } finally {
                    lock.unlock();
                }
            } else {
                System.out.println(Thread.currentThread().getName() + 
                    " 超时未能获取锁");
                handleTimeout();
            }
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + 
                " 在等待锁时被中断");
            Thread.currentThread().interrupt();
        }
    }
    
    private void handleTimeout() {
        System.out.println("处理超时情况,可能需要降级服务或返回错误");
    }
}

死锁预防示例

public class DeadlockPrevention {
    private final ReentrantLock lock1 = new ReentrantLock();
    private final ReentrantLock lock2 = new ReentrantLock();
    
    public void method1() {
        try {
            if (lock1.tryLock(1, TimeUnit.SECONDS)) {
                try {
                    System.out.println("Method1 获取 lock1");
                    Thread.sleep(100);
                    
                    if (lock2.tryLock(1, TimeUnit.SECONDS)) {
                        try {
                            System.out.println("Method1 获取 lock2");
                            // 执行需要两个锁的操作
                        } finally {
                            lock2.unlock();
                        }
                    } else {
                        System.out.println("Method1 无法获取 lock2,避免死锁");
                    }
                } finally {
                    lock1.unlock();
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
    
    public void method2() {
        try {
            if (lock2.tryLock(1, TimeUnit.SECONDS)) {
                try {
                    System.out.println("Method2 获取 lock2");
                    Thread.sleep(100);
                    
                    if (lock1.tryLock(1, TimeUnit.SECONDS)) {
                        try {
                            System.out.println("Method2 获取 lock1");
                            // 执行需要两个锁的操作
                        } finally {
                            lock1.unlock();
                        }
                    } else {
                        System.out.println("Method2 无法获取 lock1,避免死锁");
                    }
                } finally {
                    lock2.unlock();
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

实战案例:银行转账系统

让我们通过一个银行转账的例子来综合运用Lock接口的各种特性,包括超时机制、死锁预防等。

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

public class BankAccount {
    private final String accountId;
    private double balance;
    private final ReentrantLock lock = new ReentrantLock();
    
    public BankAccount(String accountId, double initialBalance) {
        this.accountId = accountId;
        this.balance = initialBalance;
    }
    
    // 安全的转账方法,使用超时机制避免死锁
    public boolean transfer(BankAccount target, double amount) {
        // 为了避免死锁,总是按照账户ID的字典序获取锁
        BankAccount firstLock = this.accountId.compareTo(target.accountId) < 0 ? this : target;
        BankAccount secondLock = this.accountId.compareTo(target.accountId) < 0 ? target : this;
        
        try {
            // 尝试获取第一个锁
            if (firstLock.lock.tryLock(1, TimeUnit.SECONDS)) {
                try {
                    // 尝试获取第二个锁
                    if (secondLock.lock.tryLock(1, TimeUnit.SECONDS)) {
                        try {
                            return performTransfer(target, amount);
                        } finally {
                            secondLock.lock.unlock();
                        }
                    } else {
                        System.out.println("转账失败:无法获取目标账户锁");
                        return false;
                    }
                } finally {
                    firstLock.lock.unlock();
                }
            } else {
                System.out.println("转账失败:无法获取源账户锁");
                return false;
            }
        } catch (InterruptedException e) {
            System.out.println("转账被中断");
            Thread.currentThread().interrupt();
            return false;
        }
    }
    
    private boolean performTransfer(BankAccount target, double amount) {
        if (this.balance >= amount) {
            this.balance -= amount;
            target.balance += amount;
            System.out.printf("转账成功:从 %s 向 %s 转账 %.2f,余额:%.2f -> %.2f%n",
                this.accountId, target.accountId, amount, this.balance + amount, this.balance);
            return true;
        } else {
            System.out.println("转账失败:余额不足");
            return false;
        }
    }
    
    public double getBalance() {
        lock.lock();
        try {
            return balance;
        } finally {
            lock.unlock();
        }
    }
    
    public String getAccountId() {
        return accountId;
    }
}

测试转账系统

public class BankTransferTest {
    public static void main(String[] args) throws InterruptedException {
        BankAccount account1 = new BankAccount("A001", 1000.0);
        BankAccount account2 = new BankAccount("A002", 1000.0);
        BankAccount account3 = new BankAccount("A003", 1000.0);
        
        // 创建多个转账线程
        Thread[] threads = new Thread[6];
        
        threads[0] = new Thread(() -> account1.transfer(account2, 100), "Transfer-1-2");
        threads[1] = new Thread(() -> account2.transfer(account1, 150), "Transfer-2-1");
        threads[2] = new Thread(() -> account1.transfer(account3, 200), "Transfer-1-3");
        threads[3] = new Thread(() -> account3.transfer(account1, 50), "Transfer-3-1");
        threads[4] = new Thread(() -> account2.transfer(account3, 75), "Transfer-2-3");
        threads[5] = new Thread(() -> account3.transfer(account2, 125), "Transfer-3-2");
        
        // 启动所有线程
        for (Thread thread : threads) {
            thread.start();
        }
        
        // 等待所有线程完成
        for (Thread thread : threads) {
            thread.join();
        }
        
        // 打印最终余额
        System.out.println("\n最终余额:");
        System.out.printf("%s: %.2f%n", account1.getAccountId(), account1.getBalance());
        System.out.printf("%s: %.2f%n", account2.getAccountId(), account2.getBalance());
        System.out.printf("%s: %.2f%n", account3.getAccountId(), account3.getBalance());
    }
}

Lock使用最佳实践

安全释放锁
  • 总是在finally块中释放锁
  • 确保每个lock()都有对应的unlock()
  • 避免在获取锁之前就进入try块
避免死锁
  • 使用tryLock()设置超时时间
  • 按固定顺序获取多个锁
  • 尽量减少锁的持有时间
性能优化
  • 根据场景选择公平锁或非公平锁
  • 考虑使用读写锁分离读写操作
  • 在低竞争场景优先使用synchronized
常见错误
  • 忘记释放锁:不在finally块中调用unlock()导致死锁
  • 重复释放锁:调用unlock()次数超过lock()次数
  • 锁泄漏:在异常情况下没有正确释放锁
  • 锁顺序不一致:多个锁的获取顺序不固定导致死锁
上一章:volatile关键字 返回目录 下一章:读写锁