【多线程】读写锁(Read-Write Lock)是什么?
【多线程】读写锁(Read-Write Lock)是什么?
本文来自于我关于多线程系列文章。欢迎阅读、点评与交流
1.【多线程】互斥锁(Mutex)是什么?
2.【多线程】临界区(Critical Section)是什么?
3.【多线程】计算机领域中的各种锁
4.【多线程】信号量(Semaphore)是什么?
5.【多线程】信号量(Semaphore)常见的应用场景
6.【多线程】条件变量(Condition Variable)是什么?
7.【多线程】监视器(Monitor)是什么?
8.【多线程】什么是原子操作(Atomic Operation)?
9.【多线程】竞态条件(race condition)是什么?
10.【多线程】无锁数据结构(Lock-Free Data Structures)是什么?
11.【多线程】线程休眠(Thread Sleep)的底层实现
12.【多线程】多线程的底层实现
13.【多线程】读写锁(Read-Write Lock)是什么?
核心概念
读写锁,也称为“共享-独占锁”,是一种同步机制,用于解决“读-写”并发场景下的性能问题。
它的核心思想是:将数据的“读”操作和“写”操作区别对待。
- 读操作(共享):不会修改数据,多个线程同时读取同一份数据不会产生问题。
- 写操作(独占):会修改数据,必须互斥地进行,并且在写的过程中,不允许任何其他线程(无论是读还是写)访问数据。
读写锁的三种状态
-
读模式加锁(共享锁)
- 当没有线程持有写锁时,任意数量的线程可以同时持有读锁。
- 这种状态下,所有读线程可以并发访问数据,大大提高了读操作的吞吐量。
-
写模式加锁(独占锁)
- 当有一个线程持有写锁时,其他任何线程(无论是读还是写)都无法获取锁,必须等待写锁被释放。
- 这种状态下,写操作是串行化的,保证了数据的一致性。
-
未加锁
- 没有任何线程持有锁。
工作原理与规则
读写锁遵循一套明确的规则来管理锁的获取:
- 读-读不互斥:多个读线程可以同时访问。这是提升性能的关键。
- 读-写互斥:如果一个线程正在读,另一个线程请求写,则写线程必须等待所有读线程释放锁。反之,如果一个线程正在写,则读线程必须等待写线程释放锁。
- 写-写互斥:同一时间只允许一个写线程进行。
为什么需要读写锁?
在没有读写锁的情况下,我们通常会使用普通的互斥锁。互斥锁的特点是,无论线程是读还是写,每次只允许一个线程访问共享资源。
问题:在读多写少的场景下(比如网站首页的访问量远大于后台管理员的修改次数),使用互斥锁会导致大量读操作之间也相互阻塞,严重限制了系统的并发性能。
读写锁的优势:在读多写少的场景中,读写锁允许多个读者同时进行,从而极大地提高了程序的并发度和吞吐量。
一个生动的比喻
把共享数据想象成一个公共黑板。
- 互斥锁:就像一把唯一的钥匙。无论你是想看黑板(读)还是擦写黑板(写),你必须拿到这把钥匙才能走到黑板前。一个人用完后,下一个人才能用。
- 读写锁:规则更智能。
- 读操作:所有人都可以同时去看黑板,不需要排队。
- 写操作:如果有人要擦写黑板,他必须等所有正在看的人都离开后,才能独自上前书写,并且在书写时,不允许其他人围观。
- 如果有人在看的时候,有人想写,那么他会阻止新来的人继续看(防止“写线程饥饿”),等当前所有看的人都离开后,他才能开始写。
潜在问题:写线程饥饿
在某些实现中,如果读线程源源不断地到来(即始终有至少一个读锁被持有),可能会导致写线程长时间甚至永远无法获取锁,这种现象称为“写线程饥饿”。
现代的读写锁实现(如 Java 的 ReentrantReadWriteLock
)通常提供了公平模式来解决这个问题。在公平模式下,锁会按照线程请求的顺序(近似地)来分配,避免了写线程的无限期等待。
代码示例(Java)
Java 中的 ReentrantReadWriteLock
是读写锁的一个经典实现。
import java.util.concurrent.locks.ReentrantReadWriteLock;public class ReadWriteLockDemo {private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();private static int sharedData = 0;public static void main(String[] args) {// 启动多个读线程for (int i = 0; i < 5; i++) {new Thread(ReadWriteLockDemo::readData, "Reader-" + i).start();}// 启动一个写线程new Thread(ReadWriteLockDemo::writeData, "Writer-1").start();// 启动另一个写线程new Thread(ReadWriteLockDemo::writeData, "Writer-2").start();}public static void readData() {lock.readLock().lock(); // 获取读锁try {System.out.println(Thread.currentThread().getName() + " is reading data: " + sharedData);Thread.sleep(1000); // 模拟读操作耗时} catch (InterruptedException e) {e.printStackTrace();} finally {lock.readLock().unlock(); // 释放读锁System.out.println(Thread.currentThread().getName() + " finished reading.");}}public static void writeData() {lock.writeLock().lock(); // 获取写锁try {System.out.println(Thread.currentThread().getName() + " is writing data.");sharedData++;Thread.sleep(2000); // 模拟写操作耗时System.out.println(Thread.currentThread().getName() + " finished writing. New data: " + sharedData);} catch (InterruptedException e) {e.printStackTrace();} finally {lock.writeLock().unlock(); // 释放写锁}}
}
可能的输出结果:
你会观察到,多个“Reader”线程几乎同时开始和结束(读读并发),而两个“Writer”线程则是严格地一个接一个执行(写写互斥),并且在写线程执行时,没有读线程能介入(读写互斥)。
总结
特性 | 互斥锁 | 读写锁 |
---|---|---|
并发性 | 低 | 高(在读多写少场景下) |
粒度 | 粗 | 细(区分读和写) |
适用场景 | 读写操作频率相当,或代码逻辑简单 | 读多写少,对读性能要求高的场景 |
总而言之,读写锁是一种非常有效的工具,它通过分离读和写操作,在保证数据一致性的前提下,显著提升了并发程序的性能。