一、锁的进化:从 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 可重入性

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

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

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

来看代码示例:

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()方法时就会死锁!

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

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获取锁]
  • 公平锁:严格按照线程请求的顺序获取锁,类似于排队买票,先来先得
  • 非公平锁:不保证等待时间最长的线程优先获取锁,允许"插队",默认模式

创建方式对比:

// 默认创建非公平锁
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()方法,我们可以实现提前结束等待状态,避免死锁:

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("程序结束");
    }
}

输出结果:

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

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

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

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

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 的超时等待特性:

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 要求手动释放锁,通常的模式是:

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

来看看它们的主要区别:

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

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

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

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 锁降级:从写锁降级为读锁

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

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 技术深度解析,欢迎点击头像关注我,后续会每日更新高质量技术文章,陪您一起进阶成长~


异常君
1 声望1 粉丝

在 Java 的世界里,永远有下一座技术高峰等着你。我愿做你登山路上的同频伙伴,陪你从看懂代码到写出让自己骄傲的代码。咱们,代码里见!