CodeGym /课程 /JAVA 25 SELF /ReentrantLock 与 ReadWriteLock:区别与示例

ReentrantLock 与 ReadWriteLock:区别与示例

JAVA 25 SELF
第 52 级 , 课程 2
可用

1. ReentrantLock 类:灵活的锁

关键字 synchronized 很适合基础场景:可以快速、简洁地保护方法或代码块。但有时我们需要更多能力:

  • 显式地管理锁(例如尝试获取,如果失败则不等待)。
  • 将对资源的“读取”和“写入”权限区分开。
  • 可中断地等待锁。
  • 诊断是谁、何时获取或释放了锁。

为此提供了类 ReentrantLockReadWriteLock。它们比传统的 synchronized 带来更多控制与功能。

它到底是什么?

ReentrantLock 是实现了接口 Lock 的一个类。它与 synchronized 的行为类似,但附带更多“增强功能”。最重要的区别在于锁的管理是显式的:需要由你自己调用 lock()unlock()

有趣的一点是,reentrant 表示“可重入”:同一个线程可以多次获取同一把锁而不会产生相互阻塞。这在方法递归调用或调用链共享同一把锁时非常有用。

用法示例

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

public class Counter {
    private int value = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock(); // 获取锁
        try {
            value++;
        } finally {
            lock.unlock(); // 务必释放锁!
        }
    }

    public int getValue() {
        lock.lock();
        try {
            return value;
        } finally {
            lock.unlock();
        }
    }
}

请注意:
调用 lock()unlock() 必须配合 try...finally 使用。若忘记调用 unlock(),其他线程将无法进入受保护的区域——会导致永久阻塞。

ReentrantLock 的功能

尝试获取:
可以尝试获取锁,但不要无限等待:

if (lock.tryLock()) {
    try {
        // 执行工作
    } finally {
        lock.unlock();
    }
} else {
    // 未能获取——去做其他事情
}

带超时的等待:

if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
    // 在 100 毫秒内获取到锁
}

检查是否已加锁:

if (lock.isLocked()) { ... }

等待队列诊断、“公平性”以及其他特性。

3. 示例:使用 ReentrantLock 递增计数器

让我们扩展一个控制台应用(例如,模拟来自不同线程的订单处理),对比使用 synchronizedReentrantLock 的写法。

使用 synchronized 的示例

public class OrderCounter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

使用 ReentrantLock 的等效示例

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

public class OrderCounter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();

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

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

有什么好处?

  • 可以尝试获取而不无限等待(tryLock())。
  • 可以实现更复杂的策略:例如按照固定顺序获取多把锁(适用于复杂数据结构)。
  • 可以在不同位置执行“解锁”(但必须非常谨慎——务必记得成对调用 unlock()!)。

4. ReadWriteLock:读写锁

这是什么?

ReadWriteLock 不只是“一把锁”,更像是一个聪明的访问调度器。它的主要实现是 ReentrantReadWriteLock,将锁分为两类:读锁与写锁。

当线程只读取且没有任何修改时,可以并发读取——读与读之间互不阻塞。但一旦有线程要修改,其他线程就必须等待:写操作只能由一个线程进行,并且需要独占。

这种方式尤其适用于读多写少的场景,例如用户经常浏览但偶尔更新的商品目录。

用法示例

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

public class ProductCatalog {
    private final Map<String, String> products = new HashMap<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void addProduct(String id, String name) {
        rwLock.writeLock().lock();
        try {
            products.put(id, name);
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    public String getProduct(String id) {
        rwLock.readLock().lock();
        try {
            return products.get(id);
        } finally {
            rwLock.readLock().unlock();
        }
    }
}

在我们的应用中的使用示例

假设我们有一个订单库,所有线程都会读取(例如搜索订单),但偶尔会有新订单写入(写操作)。

import java.util.*;
import java.util.concurrent.locks.*;

public class OrderDatabase {
    private final List<String> orders = new ArrayList<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

    // 添加订单(需要 writeLock)
    public void addOrder(String order) {
        rwLock.writeLock().lock();
        try {
            orders.add(order);
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    // 获取所有订单的副本(可并行读取)
    public List<String> getOrders() {
        rwLock.readLock().lock();
        try {
            // 返回副本以避免竞争
            return new ArrayList<>(orders);
        } finally {
            rwLock.readLock().unlock();
        }
    }
}

发生了什么?

  • 只要没有写入,即便上千个线程也能同时读取订单列表。
  • 一旦有线程开始添加订单——读操作会被阻塞,以避免得到“半更新”的数据。

5. 对比:何时使用哪一个?

场景 synchronized ReentrantLock ReadWriteLock
简单同步 ✖(不必要)
需要超时/尝试获取
读多写少 ✔(提升显著)
需要诊断/统计
可重入加锁 ✔(可重入)

结论:

  • 简单场景——使用 synchronized
  • 需要灵活性——ReentrantLock
  • “读多写少”的场景——ReadWriteLock

6. 可视化:ReadWriteLock 的工作示意

flowchart LR
    subgraph 读取
      T1[线程 1] -- 读取 --> Orders
      T2[线程 2] -- 读取 --> Orders
      T3[线程 3] -- 读取 --> Orders
    end
    subgraph 写入
      T4[线程 4] -- 写入(addOrder) --> Orders
    end
    Orders[订单列表]
    style Orders fill:#f9f,stroke:#333,stroke-width:2px

只要没有线程在写,大家都可以同时读取。一旦出现写入,其它线程将等待直到 writeLock 释放。

7. 实现细节与注意事项

“公平性”(fairness)

ReentrantLockReentrantReadWriteLock 中可以启用“公平”模式(fair mode):线程按队列顺序获得锁,而不是“谁抢到谁用”。这能避免线程“饥饿”,但可能降低性能。

Lock fairLock = new ReentrantLock(true); // true — 公平模式
ReadWriteLock fairRWLock = new ReentrantReadWriteLock(true);

潜在陷阱

  • 忘记 unlock:如果不调用 unlock(),会导致永久阻塞。务必使用 try...finally
  • 在持锁代码中抛出异常:即使代码块中发生了异常,也必须释放锁!
  • 过度使用 ReadWriteLock:对于小型集合或几乎总是写入的场景,使用 ReadWriteLock 意义不大,反而让代码更复杂。

8. 常见错误

错误 1:忘记调用 unlock()
最常见也最阴险的错误是:获取锁后忘记调用 unlock()。结果就是永久阻塞,线程一直“挂起”。无论看起来多么安全,都要使用 try...finally

错误 2:在不需要的地方使用 ReadWriteLock
如果几乎没有并发读取,而写入很频繁,ReadWriteLock 只会让代码更复杂并降低性能。仅在确实有大量并发读者时再使用它。

错误 3:以不同顺序获取多把锁
如果你的代码需要获取多把 Lock(例如针对多个对象),请在所有线程中始终以相同的顺序获取。否则可能出现 deadlock —— 线程会相互一直等待。

错误 4:仅仅为了“替换”而把 synchronized 换成 ReentrantLock
不要盲目把所有 synchronized 都换成 Lock —— 这并不一定会加速程序,反而可能让代码更难读。

错误 5:忽略可重入性
同一线程连续多次调用 lock() 对于 ReentrantLock 来说是正常的,但别忘了需要调用同样次数的 unlock()

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION