ReadWriteLock场景应用

Lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁类似,锁本身也应该是一个对象。两个线程执行的代码片段要实现同步互斥的效果,它们必须用同一个Lock对象。
  读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,我们只要上好相应的锁即可。如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!
读写锁接口:ReadWriteLock,它的具体实现类为:ReentrantReadWriteLock
       在多线程的环境下,对同一份数据进行读写,会涉及到线程安全的问题。比如在一个线程读取数据的时候,另外一个线程在写数据,而导致前后数据的不一致性;一个线程在写数据的时候,另一个线程也在写,同样也会导致线程前后看到的数据的不一致性。
       这时候可以在读写方法中加入互斥锁,任何时候只能允许一个线程的一个读或写操作,而不允许其他线程的读或写操作,这样是可以解决这样以上的问题,但是效率却大打折扣了。因为在真实的业务场景中,一份数据,读取数据的操作次数通常高于写入数据的操作,而线程与线程间的读读操作是不涉及到线程安全的问题,没有必要加入互斥锁,只要在读-写,写-写期间上锁就行了。
对于以上这种情况,读写锁是最好的解决方案!其中它的实现类:ReentrantReadWriteLock--顾名思义是可重入的读写锁,允许多个读线程获得ReadLock,但只允许一个写线程获得WriteLock
读写锁的机制:
   "读-读" 不互斥
   "读-写" 互斥
   "写-写" 互斥
 
ReentrantReadWriteLock会使用两把锁来解决问题,一个读锁,一个写锁。
  线程进入读锁的前提条件:
        1. 没有其他线程的写锁
    2. 没有写请求,或者有写请求但调用线程和持有锁的线程是同一个线程
  进入写锁的前提条件:
    1. 没有其他线程的读锁
    2. 没有其他线程的写锁
 
需要提前了解的概念:

  锁降级:从写锁变成读锁;

  锁升级:从读锁变成写锁。

  读锁是可以被多线程共享的,写锁是单线程独占的。也就是说写锁的并发限制比读锁高,这可能就是升级/降级名称的来源。

  如下代码会产生死锁,因为同一个线程中,在没有释放读锁的情况下,就去申请写锁,这属于锁升级,ReentrantReadWriteLock是不支持的。

 ReadWriteLock rtLock = new ReentrantReadWriteLock();
 rtLock.readLock().lock();
 System.out.println("get readLock.");
 rtLock.writeLock().lock();
 System.out.println("blocking");

  ReentrantReadWriteLock支持锁降级,如下代码不会产生死锁。

ReadWriteLock rtLock = new ReentrantReadWriteLock();
rtLock.writeLock().lock();
System.out.println("writeLock");

rtLock.readLock().lock();
System.out.println("get read lock");

  以上这段代码虽然不会导致死锁,但没有正确的释放锁。从写锁降级成读锁,并不会自动释放当前线程获取的写锁,仍然需要显示的释放,否则别的线程永远也获取不到写锁。

 
============以下我会通过一个真实场景下的缓存机制来讲解 ReentrantReadWriteLock 实际应用============
首先来看看ReentrantReadWriteLock的javaodoc文档中提供给我们的一个很好的Cache实例代码案例:
复制代码
 1 class CachedData {
 2   Object data;
 3   volatile boolean cacheValid;
 4   final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
 5 
 6   public void processCachedData() {
 7     rwl.readLock().lock();
 8     if (!cacheValid) {
 9       // Must release read lock before acquiring write lock
10       rwl.readLock().unlock();
11       rwl.writeLock().lock();
12       try {
13         // Recheck state because another thread might have,acquired write lock and changed state before we did.
14         if (!cacheValid) {
15           data = ...
16           cacheValid = true;
17         }
18         // 在释放写锁之前通过获取读锁降级写锁(注意此时还没有释放写锁)
19         rwl.readLock().lock();
20       } finally {
21         rwl.writeLock().unlock(); // 释放写锁而此时已经持有读锁
22       }
23     }
24 
25     try {
26       use(data);
27     } finally {
28       rwl.readLock().unlock();
29     }
30   }
31 }
复制代码

以上代码加锁的顺序为:

  1. rwl.readLock().lock();

  2. rwl.readLock().unlock();

  3. rwl.writeLock().lock();

  4. rwl.readLock().lock();

  5. rwl.writeLock().unlock();

  6. rwl.readLock().unlock();

以上过程整体讲解:

1. 多个线程同时访问该缓存对象时,都加上当前对象的读锁,之后其中某个线程优先查看data数据是否为空。【加锁顺序序号:1 】

2. 当前查看的线程发现没有值则释放读锁立即加上写锁,准备写入缓存数据。(不明白为什么释放读锁的话可以查看上面讲解进入写锁的前提条件)【加锁顺序序号:2和3 】

3. 为什么还会再次判断是否为空值(!cacheValid)是因为第二个、第三个线程获得读的权利时也是需要判断是否为空,否则会重复写入数据。

4. 写入数据后先进行读锁的降级后再释放写锁。【加锁顺序序号:4和5 】

5. 最后数据数据返回前释放最终的读锁。【加锁顺序序号:6 】

  如果不使用锁降级功能,如先释放写锁,然后获得读锁,在这个get过程中,可能会有其他线程竞争到写锁 或者是更新数据 则获得的数据是其他线程更新的数据,可能会造成数据的污染,即产生脏读的问题。

下面,让我们来实现真正趋于实际生产环境中的缓存案例:

复制代码
 1 import java.util.HashMap;
 2 import java.util.Map;
 3 import java.util.concurrent.locks.ReadWriteLock;
 4 import java.util.concurrent.locks.ReentrantReadWriteLock;
 5 
 6 public class CacheDemo {
 7     /**
 8      * 缓存器,这里假设需要存储1000左右个缓存对象,按照默认的负载因子0.75,则容量=750,大概估计每一个节点链表长度为5个
 9      * 那么数组长度大概为:150,又有雨设置map大小一般为2的指数,则最近的数字为:128
10      */
11     private Map<String, Object> map = new HashMap<>(128);
12     private ReadWriteLock rwl = new ReentrantReadWriteLock();
13     public static void main(String[] args) {
14 
15     }
16     public Object get(String id){
17         Object value = null;
18         rwl.readLock().lock();//首先开启读锁,从缓存中去取
19         try{
20             value = map.get(id);
21             if(value == null){  //如果缓存中没有释放读锁,上写锁
22                 rwl.readLock().unlock();
23                 rwl.writeLock().lock();
24                 try{
25                     if(value == null){ //防止多写线程重复查询赋值
26                         value = "redis-value";  //此时可以去数据库中查找,这里简单的模拟一下
27                     }
28                     rwl.readLock().lock(); //加读锁降级写锁,不明白的可以查看上面锁降级的原理与保持读取数据原子性的讲解
29                 }finally{
30                     rwl.writeLock().unlock(); //释放写锁
31                 }
32             }
33         }finally{
34             rwl.readLock().unlock(); //最后释放读锁
35         }
36         return value;
37     }
38 }
复制代码
<think>首先,用户的问题是:“我想理解读写锁的应用场景及使用方式 请问读写锁的使用场景及实现原理”。这是基于系统级指令的,所以我需要遵循所有规则。 系统级指令: 1. 所有行内数学表达式必须使用 $...$ 格式。 2. 独立公式必须使用 $$...$$ 格式并单独成段。 3. LaTeX 语法正确。 4. 使用中文回答。 5. 生成相关问题。 6. 回答中引用的段落末尾自然地添加引用标识,例如 [^1]。 用户可见层: - 回答结构清晰,帮助用户逐步解决问题。 - 尽量保证回答真实可靠。 参考的引用: - [^1]: 关于读写锁的使用规则和适用场景。 - [^2]: 实现读写锁的 pthread 函数。 - [^3]: 读写锁的特性、场景和优先级。 - [^4]: 读写锁的接口和手写实现。 用户的问题焦点是读写锁的应用场景和使用方式,包括实现原理。 结构回答: 1. **介绍读写锁**:简要解释什么是读写锁。 2. **应用场景**:基于引用,描述读写锁的适用场景,特别是读多写少的情况。 3. **使用方式**:解释如何使用读写锁,包括加锁和解锁操作。 4. **实现原理**:讨论读写锁的内部工作原理,如共享读、独占写。 5. **生成相关问题**:在回答结束后,添加相关问题。 确保使用中文。 在回答中引用段落末尾添加引用标识: - 例如,当提到使用规则时,引用 [^1]。 - 当提到 pthread 实现时,引用 [^2]。 - 当提到特性时,引用 [^3]。 - 当提到接口时,引用 [^4]。 数学表达式:用户的问题可能不涉及数学,但如果有变量或简单表达式,使用 $...$。例如,如果有计数器,可以写为 $count$。 现在,规划回答内容: - **什么是读写锁?**:读写锁是一种同步机制,允许多个线程同时读取共享资源,但只允许一个线程写入资源。这提高了并发性,特别是在读操作远多于写操作的场景 [^1]。 - **应用场景**:当数据被频繁读取但很少修改时,如缓存系统、配置文件读取、数据库查询等。例如,在Web服务器中,多个线程可能同时读取缓存数据 [^1][^3]。 - **使用方式**: - 使用 pthread 库:pthread_rwlock_init() 初始化,pthread_rwlock_rdlock() 加读锁,pthread_rwlock_wrlock() 加写锁,pthread_rwlock_unlock() 解锁 [^2]。 - 规则:读锁可以共享(多个线程同时持有),写锁是独占的(只能有一个线程持有),且写锁请求会阻塞新读锁请求 [^3]。 - **实现原理**: - 读写锁内部维护一个状态:可能包括读计数器、写标志等。 - 当加读锁时:如果没有写锁,读计数器增加;否则阻塞。 - 当加写锁时:如果没有读锁或写锁,设置写标志;否则阻塞。 - 优先级:某些实现中,写锁有更高优先级,以防止读锁饥饿 [^3]。 - 手写实现:可以使用互斥锁和条件变量来实现读写锁接口 [^4]。 生成相关问题:在回答结束时,添加3-4个相关问题,例如: 1. 读写锁如何避免死锁? 2. 读写锁在不同编程语言中的实现差异? 3. 读写锁的性能开销如何? 确保回答真实可靠:基于提供的引用和常识。 最终输出格式: - 回答部分:结构清晰。 - 相关问题:以 "§§相关问题§§" 开头。</think>### 读写锁的应用场景及实现原理 读写锁(Read-Write Lock)是一种高效的同步机制,用于管理共享资源的并发访问。它允许多个线程同时读取资源(共享模式),但只允许一个线程写入资源(独占模式)。这种设计特别适用于读操作远多于写操作的场景,能显著提升系统并发性能。下面我将从应用场景、使用方式和实现原理三个方面逐步解释。 #### 1. **读写锁的应用场景** 读写锁的核心优势在于优化读多写少的场景: - **高并发读取场景**:当多个线程需要频繁读取共享数据(如配置信息、缓存数据或数据库查询),但写入操作较少时,读写锁允许多个读线程并行执行,避免不必要的阻塞。例如: - 缓存系统:多个线程同时读取缓存数据,但更新缓存时需独占写入 [^1]。 - 配置文件加载:系统启动时多个线程并发读取配置,但配置变更时需单线程写入 [^3]。 - 数据库查询:在OLTP(在线事务处理)系统中,读查询远多于写操作,读写锁可提高吞吐量 [^1]。 - **避免写操作干扰**:在需要保证数据一致性的场景中,写操作会独占资源,确保写入期间数据不被脏读。例如,日志系统写入新条目时,需阻塞其他读/写操作 [^3]。 - **不适用场景**:如果写操作频繁(如实时数据更新),读写锁可能因写锁阻塞导致性能下降;此时,互斥锁或其他机制更合适 [^1]。 读写锁的使用规则可总结为:读锁共享、写锁独占、写的优先级高。具体行为包括: - 多个读锁可并行(共享模式)。 - 写锁会阻塞所有其他锁(独占模式)。 - 写请求通常有更高优先级,以防止读线程“饿死”写线程 [^3]。 #### 2. **读写锁的使用方式** 读写锁通过标准API操作,以C语言的pthread库为例: - **初始化**:使用 `pthread_rwlock_init()` 创建读写锁对象。 - **加锁操作**: - 读锁:`pthread_rwlock_rdlock()` – 允许多个线程同时加锁,用于读取数据。 - 写锁:`pthread_rwlock_wrlock()` – 仅允许一个线程加锁,用于写入数据。 - **解锁操作**:`pthread_rwlock_unlock()` – 释放锁,无论读锁或写锁。 - **销毁**:`pthread_rwlock_destroy()` – 释放锁资源。 **使用示例**: ```c #include <pthread.h> pthread_rwlock_t rwlock; // 定义读写锁 void* reader_thread(void* arg) { pthread_rwlock_rdlock(&rwlock); // 加读锁 // 读取共享数据 pthread_rwlock_unlock(&rwlock); // 解锁 return NULL; } void* writer_thread(void* arg) { pthread_rwlock_wrlock(&rwlock); // 加写锁 // 修改共享数据 pthread_rwlock_unlock(&rwlock); // 解锁 return NULL; } int main() { pthread_rwlock_init(&rwlock, NULL); // 初始化 // 创建线程并启动 pthread_rwlock_destroy(&rwlock); // 销毁 return 0; } ``` **关键规则**: - 读锁共享:多个线程可同时加读锁(例如,线程A加读锁后,线程B加读锁成功) [^3]。 - 写锁独占:写锁会阻塞所有新读/写请求(例如,线程A加写锁时,线程B的读锁请求被阻塞) [^1]。 - 优先级:写锁请求优先于读锁,以避免写操作被无限延迟(例如,线程A持有读锁时,线程B的写锁请求会阻塞后续读锁请求) [^3]。 #### 3. **读写锁的实现原理** 读写锁的内部实现依赖于状态机模型,通常使用计数器、互斥锁和条件变量构建。核心原理是管理读计数器和写标志: - **状态变量**: - 读计数器 $r$:记录当前持有读锁的线程数。 - 写标志 $w$:布尔值,表示是否有线程持有写锁($w=1$ 时,写锁活跃)。 - **加锁逻辑**: - 读锁:如果没有写锁($w=0$),则增加读计数器 $r$(允许多线程共享);否则阻塞线程。 - 写锁:如果没有读锁($r=0$)且没有写锁($w=0$),则设置 $w=1$(独占模式);否则阻塞线程。 - **解锁逻辑**: - 释放读锁:减少 $r$;如果 $r=0$,唤醒等待的写线程。 - 释放写锁:设置 $w=0$,唤醒所有等待的读/写线程。 - **优先级处理**:许多实现(如pthread)赋予写锁更高优先级。例如,当写锁请求等待时,新读锁请求会被阻塞,确保写操作不被“饿死” [^3]。 **手写实现示例**(基于Java风格的接口): 读写锁可手动实现为 `ReadWriteLock` 接口,包含读锁和写锁: ```java public class SimpleReadWriteLock implements ReadWriteLock { private int readers = 0; private boolean writer = false; private final Object lock = new Object(); // 互斥锁 public Lock readLock() { return new Lock() { public void lock() { synchronized (lock) { while (writer) { // 等待写锁释放 try { lock.wait(); } catch (InterruptedException e) {} } readers++; // 增加读计数器 } } public void unlock() { synchronized (lock) { readers--; if (readers == 0) lock.notifyAll(); // 唤醒等待线程 } } }; } public Lock writeLock() { return new Lock() { public void lock() { synchronized (lock) { while (writer || readers > 0) { // 等待无锁状态 try { lock.wait(); } catch (InterruptedException e) {} } writer = true; // 设置写标志 } } public void unlock() { synchronized (lock) { writer = false; lock.notifyAll(); // 唤醒所有线程 } } }; } } ``` 此实现使用互斥锁(`synchronized`)和条件变量(`wait/notify`)管理状态,确保原子性和线程安全 [^4]。实际中,操作系统级实现(如pthread)会优化性能,减少上下文切换开销。 #### 总结 读写锁通过区分读/写操作,在保证数据一致性的同时,提升读密集型应用的并发性。其核心是“读共享、写独占”的原则,实现依赖状态计数和优先级调度。在开发中,优先使用标准库(如pthread)以避免错误;手写实现时需注意死锁和性能问题 [^1][^2][^3]。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值