Skip to content

Lock接口

前置知识

在阅读本章前,你需要了解:

  • Java中的基本线程概念(Thread)
  • synchronized 关键词的基础用法与局限
  • 并发编程的基本需求:互斥、同步、避免死锁

为什么需要 Lock 接口?

你可能有过这样的困扰:用synchronized控制并发时,代码写得复杂且灵活性不足。比如,无法中断等待锁的线程,也没法尝试非阻塞地请求锁。或者你需要更细粒度地控制锁的获取和释放,以及条件等待通知机制。

这时候,Java的Lock接口就像是给线程加锁升级了——它让我们像开车一样,掌控锁的“油门”、“刹车”和“方向盘”。Lock不仅支持独占锁,还有读写锁这样的高级锁模式,甚至可以精准控制等待和通知的时机。

本章让我们从“锁的智慧”这个问题切入:如何用Lock接口实现比sychronized更灵活、更高效的线程同步机制。

具体章节

ReentrantLock——灵活的独占锁

什么是 ReentrantLock?

简单来说,它就是synchronized的“加强版”。它支持:

  • 可重入性:同一个线程多次获取锁不会死锁。
  • 灵活锁操作:可以尝试获取锁(非阻塞)、中断获取锁。
  • 读写锁逻辑可以基于它来实现。

为什么需要 ReentrantLock ?

synchronized你只能阻塞等待锁释放,若它等待时间过长,无法中断或者超时退出。ReentrantLock 允许你通过tryLock()尝试获取锁,避免线程因锁竞争饿死,还可以对等待线程进行中断,灵活度大大提升。

基础用法示例

java
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++;
            System.out.println(Thread.currentThread().getName() + " incremented counter to " + counter);
        } finally {
            lock.unlock(); // 确保释放锁
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockExample example = new ReentrantLockExample();

        Runnable task = () -> {
            for (int i = 0; i < 5; i++) {
                example.increment();
                try {
                    Thread.sleep(100); // 模拟工作
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        };

        Thread t1 = new Thread(task, "Thread-A");
        Thread t2 = new Thread(task, "Thread-B");

        t1.start();
        t2.start();

        t1.join();
        t2.join();
    }
}

这段代码做了什么:

  • lock.lock():请求锁,如果锁被占用,当前线程会等待。
  • 进入try块后安全地修改共享变量counter
  • finally块释放锁,哪怕代码抛异常也不漏锁。
  • 两个线程交替增加计数,保证了线程安全。

这就是使用ReentrantLock的最基本套路,跟synchronized相比多了灵活性。

进阶:读写锁 ReadWriteLock —— 分离读写 更高效

*想象图书馆的阅览室,读者可以同时读(共享),但写者需要独占(互斥)——这就是读写锁的设计初衷。*

什么是读写锁?

Java的ReadWriteLock接口定义了两个锁:只读锁和写锁。多个线程可以同时持有读取锁,但写锁是独占的。通过分离“读”和“写”操作,读写锁在读多写少的场景下极大提升并发性能。

为什么需要它?

假设一个缓存系统,频繁查询(读),偶尔更新(写)。如果所有操作都用独占锁,查询速度慢成灾。用读写锁,多个查询并发执行,大幅提升吞吐。

代码示例:使用 ReentrantReadWriteLock

java
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.Lock;

public class ReadWriteLockExample {
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();

    private int sharedData = 0;

    // 读方法:加读锁,多个线程可同时读
    public int readData() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " read sharedData: " + sharedData);
            return sharedData;
        } finally {
            readLock.unlock();
        }
    }

    // 写方法:加写锁,独占访问
    public void writeData(int newValue) {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " writing sharedData: " + newValue);
            sharedData = newValue;
            // 模拟写操作耗时
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        ReadWriteLockExample example = new ReadWriteLockExample();

        // 测试读写并发
        Runnable reader = () -> {
            for (int i = 0; i < 5; i++) {
                example.readData();
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        };

        Runnable writer = () -> {
            for (int i = 0; i < 5; i++) {
                example.writeData(i);
            }
        };

        Thread writerThread = new Thread(writer, "Writer");
        Thread readerThread1 = new Thread(reader, "Reader-1");
        Thread readerThread2 = new Thread(reader, "Reader-2");

        readerThread1.start();
        readerThread2.start();
        writerThread.start();
    }
}

这段代码做了什么:

  • 写线程独占写锁修改数据。
  • 多个读线程可以并发访问共享数据。
  • 代码演示了读写锁如何在读写混合场景中协调。

这能帮你理解什么时候分离读写能带来性能红利。

Condition接口——更精细的等待通知控制

是不是觉得synchronizedwait/notify有点生硬?Condition就像给Lock配了个更灵活的“红绿灯”。

什么是 Condition?

它是Lock接口里定义的“等待/通知”工具,可以创建多个条件队列,使用起来比synchronized的单一等待队列更灵活。

为什么需要 Condition?

你可以有选择地等待某个条件,并且多个线程可以各自等待不同条件,避免无谓的唤醒(降低虚假唤醒的风险)。

代码示例:用 Condition 实现简单生产者消费者

java
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionExample {
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();   // 生产者等待条件
    private final Condition notEmpty = lock.newCondition();  // 消费者等待条件

    private final int[] buffer = new int[5];
    private int count = 0, putIndex = 0, takeIndex = 0;

    // 生产者放入元素
    public void put(int value) throws InterruptedException {
        lock.lock();
        try {
            while (count == buffer.length) {
                System.out.println("缓冲区满,生产者等待...");
                notFull.await();  // 等待缓冲区有空位
            }
            buffer[putIndex] = value;
            putIndex = (putIndex + 1) % buffer.length;
            count++;
            System.out.println("生产者放入: " + value);
            notEmpty.signal();  // 通知消费者有新元素
        } finally {
            lock.unlock();
        }
    }

    // 消费者取出元素
    public int take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                System.out.println("缓冲区空,消费者等待...");
                notEmpty.await(); // 等待缓冲区有元素
            }
            int value = buffer[takeIndex];
            takeIndex = (takeIndex + 1) % buffer.length;
            count--;
            System.out.println("消费者取出: " + value);
            notFull.signal();  // 通知生产者缓冲区有空位
            return value;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ConditionExample buffer = new ConditionExample();

        Runnable producer = () -> {
            int i = 0;
            try {
                while (true) {
                    buffer.put(i++);
                    Thread.sleep(200);  // 模拟生产耗时
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        };

        Runnable consumer = () -> {
            try {
                while (true) {
                    buffer.take();
                    Thread.sleep(500);  // 模拟消费耗时
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        };

        new Thread(producer, "Producer").start();
        new Thread(consumer, "Consumer").start();
    }
}

这段代码做了什么:

  • 生产者在缓冲区满时等待notFull条件,缓冲区空时消费者等待notEmpty条件。
  • 使用两个不同的条件队列分开控制生产者和消费者。
  • 更细致的等待/通知控制避免了传统wait()/notify()的“通知全体”弊端。

tryLock() — 试探式锁的妙用

情况描述

某些时候,你不想让线程永远阻塞在获取锁上,而是希望尝试获取锁,如果失败就做别的事情或者稍后重试。

tryLock()正好满足这点。

代码示例:使用tryLock避免死锁

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

public class TryLockExample {
    private final Lock lock = new ReentrantLock();

    public void doWork() {
        // 试图获取锁,等待最多1秒
        try {
            if (lock.tryLock(1, TimeUnit.SECONDS)) {
                try {
                    System.out.println(Thread.currentThread().getName() + " 获得锁,开始工作");
                    Thread.sleep(2000); // 模拟工作
                } finally {
                    lock.unlock();
                    System.out.println(Thread.currentThread().getName() + " 释放锁");
                }
            } else {
                System.out.println(Thread.currentThread().getName() + " 未能获得锁,执行其他任务");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public static void main(String[] args) {
        TryLockExample example = new TryLockExample();

        Runnable task = example::doWork;

        Thread t1 = new Thread(task, "线程1");
        Thread t2 = new Thread(task, "线程2");

        t1.start();
        t2.start();
    }
}

这段代码做了什么:

  • 线程尝试获取锁,等待最多1秒。
  • 成功后执行任务,失败则打印出放弃提示。
  • 避免了无限制等待和死锁风险。

⚠️ 常见陷阱

  • 忘记释放锁lock.lock()后务必放在finally块中调用unlock(),否则会导致死锁。
  • 使用错误的锁粒度:锁范围过大影响性能,过小则无法保证数据一致。
  • Condition滥用:在使用多条件时,唤醒和等待必须严格匹配,否则会造成线程永久等待。
  • 读写锁不适合写多读少场景:写频繁时会导致写锁阻塞,反而比简单锁性能差。
  • tryLock误用导致逻辑混乱:因为是非阻塞,需准备好失败处理逻辑,否则可能引发数据不一致。

💡 实战建议

  • 优先用synchronized实现简单互斥,明确性能瓶颈后再考虑Lock
  • 使用ReentrantLock时,牢记必须配对lock()unlock()
  • 读多写少场景下,优先ReentrantReadWriteLock,合理划分读写操作。
  • Condition实现精细等待通知时,更关注逻辑状态而非直接调用signal(),防止“虚假唤醒”。
  • 采用tryLock()与超时接口,避免线程死锁和饥饿状态。
  • 实际线上使用锁时,尽可能减少锁持有时间,释放资源要及时。
🔍 深入理解
  • ReentrantLock底层采用了AQS (AbstractQueuedSynchronizer) 实现,这就是它能支持阻塞队列和公平策略的秘密。
  • 读写锁ReentrantReadWriteLock中读锁是共享锁,写锁是独占锁,底层通过计数器维护读写状态。
  • Condition的细节包括单独的等待队列,线程在await()时会释放当前锁,放入等待队列,signal()才会唤醒一个等待线程重新竞争锁。
  • tryLock的超时版本内部调用了线程park/unpark机制,配合FIFO同步队列实现等待队列管理。

实战应用

想象你正在开发一个高性能缓存模块:

  • 读写操作高度竞争,适合用ReentrantReadWriteLock分离读写。
  • 缓存失效时写入数据时,可以用Condition实现等待策略,让缓存更新线程等待其他线程完成。
  • 关键任务路径上,用tryLock避免因锁阻塞影响用户体验,失败后可启用降级方案。

以上Lock接口及其功能的组合,能帮助你写出既安全又高效的并发代码。

小结

  • ReentrantLock提供比synchronized更灵活的锁操作,如中断响应、超时等待、非阻塞获取。
  • 读写锁能显著提高读多写少场景的性能,分离读锁和写锁。
  • Condition接口扩展了等待/通知机制,支持多条件同时等待和精细唤醒。
  • tryLock让线程可以非阻塞尝试锁资源,减少死锁风险和性能瓶颈。
  • 使用锁时,严格遵守加锁-释放锁的规范,合理设计锁粒度、使用合适的锁类型。

通过这些内容,锁的魔法在你手中更加灵活与强大。但记住,锁是个双刃剑,用好了是保护神,用不好可能成“死锁制造机”。愿你写出既稳定又高效的并发程序!


如果你对某个具体锁的性能细节或者内部原理好奇,我们可以再深入探讨。现在,就让我们把这把“锁”练得更熟练一些吧!