深入剖析 Java ReentrantLock:解锁显式锁的高级特性与实战应用

一、锁的进化:从 synchronized 到 ReentrantLock

大家好,在多线程编程中,锁机制是保证线程安全的核心技术。Java 早期只提供了 synchronized 这一种内置锁,而在 JDK 1.5 后,Doug Lea 大师为我们带来了更加灵活强大的显式锁ReentrantLock

synchronized 虽然用起来简单,但在某些场景下会显得"能力不足":

  • 无法响应中断请求
  • 无法尝试获取锁
  • 不支持公平性选择
  • 通知机制基于单一等待队列,难以实现精准唤醒

这时,ReentrantLock就成了我们的"救星"。让我们一起来深入了解这把锁!

二、ReentrantLock 的核心特性

ReentrantLock 是 Lock 接口的一个实现,它提供了比 synchronized 更丰富的功能:

graph TD A[Lock接口] --> B[ReentrantLock] B --> C[公平锁] B --> D[非公平锁] A --> E[ReadWriteLock接口] --> F[ReentrantReadWriteLock] A -.-> G[其他实现...]

2.1 可重入性

首先,什么是"可重入"?简单说就是:同一个线程可以多次获取同一把锁而不会死锁

举个生活例子:小明进入自己房间后反锁了门,这时他想去卫生间,卫生间的门也是需要钥匙的,而这把钥匙就在小明口袋里。如果锁是"不可重入"的,那么小明就陷入了困境------他无法使用口袋里的钥匙,因为他已经在使用这把钥匙锁住了房门。

但在"可重入锁"的情况下,小明可以直接用同一把钥匙开卫生间的门,而不会有任何问题。

来看代码示例:

java 复制代码
public class ReentrantDemo {
    private final ReentrantLock lock = new ReentrantLock();

    public void outer() {
        lock.lock();  // 第一次获取锁
        try {
            System.out.println("进入outer方法,当前线程:" + Thread.currentThread().getName());
            inner();  // 调用inner方法
        } finally {
            lock.unlock();  // 释放锁
        }
    }

    public void inner() {
        lock.lock();  // 第二次获取锁(同一线程)
        try {
            System.out.println("进入inner方法,当前线程:" + Thread.currentThread().getName());
        } finally {
            lock.unlock();  // 释放锁
        }
    }

    public static void main(String[] args) {
        ReentrantDemo demo = new ReentrantDemo();
        demo.outer();
    }
}

如果没有可重入特性,上面代码在调用 inner()方法时就会死锁!

为了更直观地理解可重入性的重要性,看一个模拟"不可重入锁"的例子:

java 复制代码
public class NonReentrantLockDemo {
    // 模拟一个不可重入锁
    private static class NonReentrantLock {
        private boolean isLocked = false;
        private Thread lockedBy = null;

        public synchronized void lock() throws InterruptedException {
            // 不管是否是当前持有锁的线程,都要等待锁释放
            while (isLocked) {
                wait();
            }
            isLocked = true;
            lockedBy = Thread.currentThread();
        }

        public synchronized void unlock() {
            if (isLocked && Thread.currentThread() == lockedBy) {
                isLocked = false;
                lockedBy = null;
                notify();
            }
        }
    }

    private static final NonReentrantLock nonReentrantLock = new NonReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        nonReentrantLock.lock();
        System.out.println("获取第一次锁");

        try {
            // 尝试再次获取锁
            System.out.println("尝试获取第二次锁...");
            nonReentrantLock.lock();  // 这里会永久阻塞!
            System.out.println("获取第二次锁成功"); // 永远不会执行到这里
        } finally {
            nonReentrantLock.unlock();
        }
    }
}

运行这段代码会永久阻塞,因为第二次调用lock()时,锁已被同一线程持有,但由于不支持重入,线程只能等待自己释放锁,形成死锁。这正是可重入性解决的问题。

2.2 公平锁与非公平锁

ReentrantLock 提供了两种获取锁的方式:公平锁和非公平锁。

graph TD A[ReentrantLock] --> B[非公平锁默认] A --> C[公平锁] B -- "lock()" --> D[立即尝试抢占锁] D -- "失败" --> E[进入队列等待] C -- "lock()" --> F[严格按照等待队列FIFO获取锁]
  • 公平锁:严格按照线程请求的顺序获取锁,类似于排队买票,先来先得
  • 非公平锁:不保证等待时间最长的线程优先获取锁,允许"插队",默认模式

创建方式对比:

java 复制代码
// 默认创建非公平锁
ReentrantLock unfairLock = new ReentrantLock();

// 创建公平锁
ReentrantLock fairLock = new ReentrantLock(true);

公平锁的优点是显著降低了"饥饿"现象发生的概率,保证每个线程都有机会获取锁;缺点是整体吞吐量相对较低。非公平锁则允许更充分地利用 CPU 资源,但可能导致某些线程长时间等待。

需要注意的是,即使使用公平锁,也无法完全杜绝饥饿现象,因为线程可能因为其他原因(如中断或取消)退出等待队列。

场景选择建议

  • 在高并发且线程生命周期较短的场景中,非公平锁通常表现更好,因为新线程可以立即尝试获取锁,减少上下文切换
  • 在线程任务执行时间差异大、并且某些线程优先级较低的系统中,公平锁可以减少低优先级线程的饥饿概率
  • 对于需要严格保证请求顺序的系统(如排队系统),公平锁是更合适的选择

2.3 多种获取锁的方式

ReentrantLock 提供了多种获取锁的方式,大大增强了灵活性:

  1. lock():最基本的获取锁方法,如果锁被占用,会一直等待
  2. tryLock():尝试获取锁,立即返回结果(成功/失败),不会阻塞
  3. tryLock(long timeout, TimeUnit unit):在指定时间内尝试获取锁
  4. lockInterruptibly():可中断的获取锁,允许在等待时响应中断信号

我们可以用一个餐厅排队的例子来理解:

  • lock():不管多久我都要等到有位置
  • tryLock():看一眼有没有空位,有就坐,没有就走
  • tryLock(time):最多等 30 分钟,如果还没位置就去别家
  • lockInterruptibly():等位过程中如果接到重要电话可以中途离开

2.4 精准通知机制:Condition

ReentrantLock 结合 Condition 接口,提供了比 synchronized + wait/notify 更加强大的线程通信能力:

graph LR A[ReentrantLock] -- "创建" --> B[Condition A] A -- "创建" --> C[Condition B] A -- "创建" --> D[Condition C] B -- "await/signal" --> E[线程1] C -- "await/signal" --> F[线程2] D -- "await/signal" --> G[线程3]

与 synchronized 相比的优势:

  • 一个锁可以创建多个 Condition 对象,实现"选择性通知"
  • 更精准的线程控制,避免了 Object.notify()的盲目唤醒
  • 提供带超时的等待和可中断的等待

信号类型对比

  • signal():只唤醒单个等待该条件的线程,适用于只需要唤醒一个消费者/生产者的场景
  • signalAll():唤醒所有等待该条件的线程,适用于需要通知所有相关线程的状态变更场景

重要提示Conditionawait()signal()方法必须在持有锁的情况下调用,否则会抛出IllegalMonitorStateException。这一点与synchronized中的wait()/notify()要求一致。

Condition 还提供了带超时的等待方法:

  • await(long time, TimeUnit unit):在指定时间内等待,超时或被通知则返回

这进一步增强了线程等待的灵活性,避免了无限期阻塞的风险。

2.5 锁状态查询能力

ReentrantLock 提供了一系列查询锁状态的方法,这在调试和监控中非常有用:

  • isLocked():查询锁是否被任何线程持有
  • isHeldByCurrentThread():查询当前线程是否持有锁
  • getHoldCount():查询当前线程持有锁的次数(重入次数)
  • getQueueLength():获取等待获取此锁的线程数
  • hasQueuedThread(Thread t):查询指定线程是否在等待队列中

这些方法让我们能够更精确地了解锁的使用状态,在复杂并发场景中进行故障排查。

三、ReentrantLock 实战案例

3.1 案例 1:实现可中断的获取锁

当多个线程竞争锁时,如果使用lockInterruptibly()方法,我们可以实现提前结束等待状态,避免死锁:

java 复制代码
public class InterruptibleLockDemo {
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            try {
                lock.lock();
                System.out.println("线程1获取到锁,将无限期持有...");
                // 模拟长时间持有锁
                Thread.sleep(Integer.MAX_VALUE);
            } catch (InterruptedException e) {
                System.out.println("线程1被中断");
                // 此处不恢复中断状态,因为线程需要继续持有锁而不被中断
            } finally {
                lock.unlock();
                System.out.println("线程1释放锁");
            }
        });

        thread1.start();
        Thread.sleep(500); // 确保线程1先获取到锁

        Thread thread2 = new Thread(() -> {
            System.out.println("线程2尝试获取锁...");
            try {
                // 可中断的获取锁
                lock.lockInterruptibly();
                System.out.println("线程2获取到锁");
            } catch (InterruptedException e) {
                System.out.println("线程2等待锁的过程被中断了");
                // 恢复中断状态
                Thread.currentThread().interrupt();
            }
        });

        thread2.start();
        Thread.sleep(1000); // 给线程2一些时间尝试获取锁

        // 中断线程2的等待
        System.out.println("主线程决定中断线程2的等待");
        thread2.interrupt();

        // 等待线程2处理完中断
        thread2.join();
        System.out.println("程序结束");
    }
}

输出结果:

erlang 复制代码
线程1获取到锁,将无限期持有...
线程2尝试获取锁...
主线程决定中断线程2的等待
线程2等待锁的过程被中断了
程序结束

这个案例说明:使用lockInterruptibly()可以避免线程无限期地等待锁,增强了程序的可控性。相比之下,如果使用lock()方法,线程 2 将无法响应中断,只能一直等待。

3.2 案例 2:使用 tryLock 实现超时等待

在一些对时间敏感的系统中,无限期等待锁可能导致严重问题。使用tryLock()方法可以设置等待超时时间:

java 复制代码
public class TryLockDemo {
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            try {
                lock.lock();
                System.out.println("线程1获取到锁");
                // 模拟持有锁的工作
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                // 恢复中断状态
                Thread.currentThread().interrupt();
            } finally {
                lock.unlock();
                System.out.println("线程1释放锁");
            }
        });

        thread1.start();

        // 确保线程1先获取到锁
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
            // 恢复中断状态
            Thread.currentThread().interrupt();
        }

        Thread thread2 = new Thread(() -> {
            boolean acquired = false;
            try {
                System.out.println("线程2尝试获取锁,最多等待2秒");
                // 尝试在2秒内获取锁
                acquired = lock.tryLock(2, TimeUnit.SECONDS);
                if (acquired) {
                    System.out.println("线程2成功获取到锁");
                    // 模拟工作
                    Thread.sleep(1000);
                } else {
                    System.out.println("线程2获取锁失败,执行备选方案");
                    // 执行其他操作...
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                // 重要:恢复中断状态,以便调用者能够检测到中断
                Thread.currentThread().interrupt();
            } finally {
                if (acquired) {
                    lock.unlock();
                    System.out.println("线程2释放锁");
                }
            }
        });

        thread2.start();
    }
}

注意上面代码中,当捕获InterruptedException时,我们调用了Thread.currentThread().interrupt()来恢复线程的中断状态。这是因为异常被捕获后,线程的中断状态会被清除,而恢复中断状态可以让上层调用者知道线程曾经被中断过。

输出结果:

复制代码
线程1获取到锁
线程2尝试获取锁,最多等待2秒
线程2获取锁失败,执行备选方案
线程1释放锁

这个案例演示了如何避免线程长时间等待,提高系统的响应性。tryLock方法在分布式系统或微服务架构中特别有用,可以防止级联阻塞。

3.3 案例 3:使用 Condition 实现精准线程通信

使用 Condition 可以实现更精细的线程控制,下面是一个使用多个 Condition 实现的有界缓冲区示例,并演示了 Condition 的超时等待特性:

java 复制代码
public class BoundedBuffer {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();   // 缓冲区不满条件
    private final Condition notEmpty = lock.newCondition();  // 缓冲区不空条件

    private final Object[] items;
    private int putIndex, takeIndex, count;

    public BoundedBuffer(int capacity) {
        items = new Object[capacity];
    }

    // 存入数据
    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            // 使用while循环检查条件,防止虚假唤醒
            while (count == items.length) {
                System.out.println(Thread.currentThread().getName() + " 发现缓冲区已满,等待...");
                notFull.await();  // 必须在持有锁的状态下调用
            }

            items[putIndex] = x;
            if (++putIndex == items.length) putIndex = 0;
            ++count;

            System.out.println(Thread.currentThread().getName() + " 放入数据: " + x +
                             ",当前缓冲区数据量: " + count);

            // 通知消费者可以取数据了
            notEmpty.signal();  // 精确通知等待缓冲区不空的线程
        } finally {
            lock.unlock();
        }
    }

    // 取出数据(带超时)
    public Object takeWithTimeout(long timeout, TimeUnit unit) throws InterruptedException {
        lock.lock();
        try {
            // 计算截止时间
            long nanos = unit.toNanos(timeout);

            // 使用while循环检查条件
            while (count == 0) {
                System.out.println(Thread.currentThread().getName() + " 发现缓冲区为空,尝试等待" +
                                  timeout + unit.toString().toLowerCase() + "...");

                if (nanos <= 0) {
                    // 超时退出
                    System.out.println(Thread.currentThread().getName() + " 等待超时,返回null");
                    return null;
                }

                // 带超时的等待,返回剩余等待时间
                nanos = notEmpty.awaitNanos(nanos);
            }

            Object x = items[takeIndex];
            if (++takeIndex == items.length) takeIndex = 0;
            --count;

            System.out.println(Thread.currentThread().getName() + " 取出数据: " + x +
                             ",当前缓冲区数据量: " + count);

            // 通知生产者可以放数据了
            notFull.signal();  // 精确通知等待缓冲区不满的线程
            return x;
        } finally {
            lock.unlock();
        }
    }

    // 唤醒所有等待的生产者(示例signalAll()用法)
    public void signalAllProducers() {
        lock.lock();
        try {
            System.out.println("唤醒所有等待的生产者线程");
            notFull.signalAll();  // 唤醒所有等待"不满"条件的线程
        } finally {
            lock.unlock();
        }
    }

    // 原始的取出方法
    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                System.out.println(Thread.currentThread().getName() + " 发现缓冲区为空,等待...");
                notEmpty.await();
            }

            Object x = items[takeIndex];
            if (++takeIndex == items.length) takeIndex = 0;
            --count;

            System.out.println(Thread.currentThread().getName() + " 取出数据: " + x +
                             ",当前缓冲区数据量: " + count);

            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }

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

        // 生产者线程(速度较慢)
        Thread producer = new Thread(() -> {
            try {
                for (int i = 1; i <= 5; i++) {
                    Thread.sleep(500);  // 生产慢一点,让消费者体验超时
                    buffer.put(i);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                Thread.currentThread().interrupt();
            }
        }, "生产者");

        // 消费者线程(带超时)
        Thread consumer = new Thread(() -> {
            try {
                for (int i = 1; i <= 10; i++) {
                    // 超时等待2秒
                    Object item = buffer.takeWithTimeout(2, TimeUnit.SECONDS);
                    if (item == null) {
                        System.out.println("消费者因超时放弃等待,循环次数: " + i);
                    }
                    Thread.sleep(100);  // 消费速度快一些
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                Thread.currentThread().interrupt();
            }
        }, "消费者");

        consumer.start();  // 先启动消费者,这样必然会遇到空缓冲区
        try {
            Thread.sleep(1000);  // 让消费者先等一会儿
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        producer.start();  // 后启动生产者
    }
}

上面代码中有几个关键点需要特别注意:

  1. 使用 while 而非 if 检查条件 :这是防止虚假唤醒(Spurious Wakeup)。线程可能在没有被显式唤醒的情况下从await()返回,使用 while 循环确保条件确实满足。

  2. await()signal()必须在持有锁的情况下调用 :这与synchronized中的wait()/notify()一样,是线程安全的基本要求。

  3. 精确通知notFull.signal()只会唤醒等待"不满"条件的生产者线程,notEmpty.signal()只会唤醒等待"不空"条件的消费者线程。这比synchronized中的notify()更有针对性。

  4. 超时等待takeWithTimeout方法展示了如何使用Condition.awaitNanos()实现带超时的等待,避免了消费者无限期等待的问题。

  5. 信号类型选择 :示例中还展示了signalAll()方法的用法,当需要唤醒多个等待线程时(如清空缓冲区操作),应使用signalAll()而非signal()

四、ReentrantLock 底层原理探秘

ReentrantLock 的强大功能离不开其底层实现机制------AQS(AbstractQueuedSynchronizer)。

graph TD A[ReentrantLock] --> B[AbstractQueuedSynchronizer AQS] B --> C[volatile int state] B --> D[FIFO双向等待队列] C --> E[state=0 表示无锁] C --> F[state>0 表示有锁] F --> G[state值=持有线程重入次数] D --> H[节点状态CANCELLED/SIGNAL等]

AQS 内部维护了一个 volatile 变量 state 和一个 FIFO 的等待队列。对于 ReentrantLock:

  • state = 0 表示锁空闲
  • state > 0 表示锁被占用,值记录了重入次数
  • 当一个线程获取锁失败时,它会被包装成一个 Node 加入 FIFO 队列
  • 队列中的节点有不同状态(如 CANCELLED、SIGNAL 等),AQS 通过这些状态管理线程的阻塞与唤醒,避免无效竞争
  • 释放锁时会唤醒队列中的后继节点

在非公平锁实现中,新到来的线程可以直接尝试 CAS 获取锁,而不必排队;在公平锁实现中,线程必须先检查队列中是否有前驱节点,只有没有前驱时才能尝试获取锁。

这种机制使得 ReentrantLock 能够高效地管理锁竞争,并支持公平或非公平获取锁的策略。

五、ReentrantLock 使用注意事项

5.1 必须手动释放锁

与 synchronized 不同,ReentrantLock 要求手动释放锁,通常的模式是:

java 复制代码
ReentrantLock lock = new ReentrantLock();
lock.lock();  // 获取锁
try {
    // 临界区代码
} finally {
    lock.unlock();  // 确保锁被释放
}

为什么要放在 finally 块中? 防止临界区代码抛出异常而导致锁无法释放,进而引发死锁。这是使用 ReentrantLock 最容易出错的地方,必须养成良好习惯。

5.2 公平锁与非公平锁的选择

  • 非公平锁(默认):吞吐量更高,但可能造成线程饥饿
  • 公平锁:等待更公平,但整体性能较低

根据 Oracle JDK 的官方基准测试,在高竞争环境下,公平锁的吞吐量比非公平锁低约 10%-20%。这是因为公平锁需要维护一个严格的 FIFO 队列,额外的检查和同步开销导致性能下降。

一般情况下使用默认的非公平锁即可,除非系统特别需要保证每个线程的公平性。

5.3 性能考量

ReentrantLock 相比 synchronized 在不同场景下的性能表现:

  • 低竞争场景:JDK 1.6 后对 synchronized 进行了大量优化(偏向锁、轻量级锁),在低竞争情况下,synchronized 性能接近甚至优于 ReentrantLock
  • 高竞争场景:ReentrantLock 的灵活性(如超时获取、可中断)和精确的线程控制能带来更好的整体性能

选择时应考虑实际应用场景和锁竞争的激烈程度。

六、ReentrantLock vs synchronized

来看看它们的主要区别:

特性 ReentrantLock synchronized
锁获取方式 显式(lock()) 隐式(进入同步块)
锁释放方式 显式(unlock()) 隐式(离开同步块)
锁类型 接口实现,可以继承 关键字,内置语言特性
可中断获取 支持(lockInterruptibly()) 不支持
超时获取 支持(tryLock(time)) 不支持
公平性 可选择(默认非公平) 非公平
多条件变量 支持(Condition) 不支持(只有一个等待集合)
性能(低竞争) 较好 JDK 1.6 优化后较好
性能(高竞争) 较好 JDK 1.6 优化后接近
锁状态检查 支持(isLocked()等) 不支持
编码复杂度 较高(需手动解锁) 较低(自动解锁)

七、ReentrantLock 进阶案例:可重入读写锁

在某些场景下,我们需要区分读操作和写操作的锁定粒度。ReentrantReadWriteLock 提供了这种能力:

java 复制代码
public class ReadWriteLockDemo {
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();

    private final Map<String, String> data = new HashMap<>();

    // 写操作:独占锁
    public void put(String key, String value) {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 正在写入数据...");
            // 模拟写入耗时
            Thread.sleep(1000);
            data.put(key, value);
            System.out.println(Thread.currentThread().getName() + " 写入完成: " + key + "=" + value);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            writeLock.unlock();
        }
    }

    // 读操作:共享锁
    public String get(String key) {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 正在读取数据...");
            // 模拟读取耗时
            Thread.sleep(200);  // 读操作比写操作快,更能体现读共享优势
            String value = data.get(key);
            System.out.println(Thread.currentThread().getName() + " 读取完成: " + key + "=" + value);
            return value;
        } catch (InterruptedException e) {
            e.printStackTrace();
            return null;
        } finally {
            readLock.unlock();
        }
    }

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

        // 预先放入一些数据
        demo.put("key1", "value1");

        // 创建10个读线程,更好地展示读并发效果
        for (int i = 0; i < 10; i++) {
            final int index = i;
            new Thread(() -> {
                demo.get("key1");
            }, "读线程" + index).start();
        }

        // 创建2个写线程
        for (int i = 0; i < 2; i++) {
            final int index = i;
            new Thread(() -> {
                demo.put("key" + (index + 2), "value" + (index + 2));
            }, "写线程" + index).start();
        }
    }
}

关键点:

  • 写锁是独占的:一次只能有一个线程获取写锁
  • 读锁是共享的:多个线程可以同时获取读锁
  • 写锁和读锁互斥:有写锁时不能获取读锁,有读锁时不能获取写锁
  • 适合"读多写少"的场景

7.1 锁降级:从写锁降级为读锁

一个重要但常被忽略的技巧是锁降级,即持有写锁的线程可以获取读锁,然后释放写锁,这样就从写锁降级为读锁了:

java 复制代码
public class LockDegradingDemo {
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();

    private Map<String, Object> cacheMap = new HashMap<>();

    // 使用锁降级更新缓存
    public Object processCachedData(String key) {
        Object value = null;

        // 首先获取读锁查询缓存
        readLock.lock();
        try {
            value = cacheMap.get(key);
            if (value == null) {
                // 缓存未命中,释放读锁,获取写锁
                readLock.unlock();
                writeLock.lock();
                try {
                    // 再次检查,因为可能其他线程已经更新了缓存
                    value = cacheMap.get(key);
                    if (value == null) {
                        // 模拟从数据库加载数据
                        value = loadFromDatabase(key);
                        cacheMap.put(key, value);
                        System.out.println("缓存更新完毕: " + key);
                    }

                    // 锁降级:持有写锁的同时获取读锁
                    readLock.lock();
                } finally {
                    // 释放写锁,保留读锁
                    writeLock.unlock();
                }
                // 此时线程仍持有读锁
            }

            // 使用读锁保护的数据
            return value;
        } finally {
            readLock.unlock();
        }
    }

    private Object loadFromDatabase(String key) {
        System.out.println("从数据库加载: " + key);
        // 模拟耗时操作
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "DB_" + key + "_VALUE";
    }

    public static void main(String[] args) {
        LockDegradingDemo demo = new LockDegradingDemo();

        // 多线程并发访问
        for (int i = 0; i < 5; i++) {
            final String key = "key" + (i % 2);  // 只使用两个不同的key,增加并发更新的可能
            new Thread(() -> {
                Object value = demo.processCachedData(key);
                System.out.println(Thread.currentThread().getName() + " 获取到: " + key + "=" + value);
            }, "Thread-" + i).start();
        }
    }
}

锁降级的好处是保证数据的可见性。在更新完数据后,如果我们先释放写锁再获取读锁,那么在这个短暂的时间窗口内,可能有其他线程修改了数据。通过锁降级,我们确保读取的是自己最新写入的数据。

八、总结

通过本文的讲解,我们全面了解了 ReentrantLock 的高级特性与应用。下表总结了 ReentrantLock 的关键特性和应用场景:

特性 方法 适用场景 注意事项
基本锁获取 lock() 一般同步场景 必须在 finally 中解锁
可重入性 内置特性 递归调用、嵌套锁 调用 unlock 次数必须等于 lock 次数
尝试获取锁 tryLock() 避免死锁、提高响应性 结果为 false 时需有备选方案
可中断锁获取 lockInterruptibly() 需要中断能力的场景 抛出 InterruptedException 后恢复中断状态
超时锁获取 tryLock(time, unit) 限时等待场景 超时返回 false
公平性控制 构造函数参数 需要减少饥饿的场景 公平锁性能约低 10%-20%
条件变量 newCondition() 复杂线程协作 await 前必须持有锁,使用 while 循环检查条件
超时等待 await(time, unit) 需限时等待的场景 返回值表示是否超时
锁状态查询 isLocked()等 调试和监控 结果可能立即过时
读写锁分离 ReentrantReadWriteLock 读多写少的场景 写锁可降级为读锁,反之不可

最后,记住一条黄金法则:锁的范围要尽可能小,持有时间要尽可能短。这样能最大限度地减少线程间的竞争,提高程序的并发性能。

在实际项目中,根据业务需求的不同,灵活选择合适的锁机制,才能构建高效、稳定的多线程应用!

在下一篇文章中,我们将探讨"线程间通信的三种经典方式",敬请期待!


感谢您耐心阅读到这里!如果觉得本文对您有帮助,欢迎点赞 👍、收藏 ⭐、分享给需要的朋友,您的支持是我持续输出技术干货的最大动力!

如果想获取更多 Java 技术深度解析,欢迎点击头像关注我,后续会每日更新高质量技术文章,陪您一起进阶成长~

相关推荐
观无几秒前
基于AOP+Log4Net+AutoFac日志框架
java·大数据·数据库
AronTing几秒前
09-RocketMQ 深度解析:从原理到实战,构建可靠消息驱动微服务
后端·面试·架构
方块海绵2 分钟前
RabbitMQ总结
后端
星辰大海的精灵3 分钟前
Python 中利用算法优化性能的方法
后端·python
雷渊4 分钟前
深度分析Scroll API(滚动搜索)方案
后端
AronTing5 分钟前
11-Spring Cloud OpenFeign 深度解析:从基础概念到对比实战
后端·spring cloud·架构
yifuweigan5 分钟前
J2Cache 实现多级缓存
后端
洛神灬殇8 分钟前
【Redis技术进阶之路】「原理分析系列开篇」探索事件驱动枚型与数据特久化原理实现(时间事件驱动执行控制)
redis·后端
Java中文社群10 分钟前
SpringAI版本更新:向量数据库不可用的解决方案!
java·人工智能·后端
日月星辰Ace11 分钟前
蓝绿部署
运维·后端