并发编程原理与实战(二十一)深入锁的演进,为什么有了ReentrantLock还需要ReadWriteLock?

ReentrantLock是一个独占锁,独占的意思是同一时间只允许一个线程访问共享资源,在某些场景中独占锁的性能并不是最好的,比如对共享资源读的时候允许多个线程同时读取,以最大化的提高系统性能,所以又推出了读写锁,本文就来学习读写锁的相关知识。

认识ReadWriteLock

/**
 * A {@code ReadWriteLock} maintains a pair of associated {@link
 * Lock locks}, one for read-only operations and one for writing.
 * The {@linkplain #readLock read lock} may be held simultaneously
 * by multiple reader threads, so long as there are no writers.
 * The {@linkplain #writeLock write lock} is exclusive.
 */

ReadWriteLock(读写锁)维护一对关联的Lock 锁,一个用于只读操作,另一个用于写入操作。只要没有写入线程,readLock 锁可被多个读线程同时持有;而writeLock 写锁是独占的。

 /* <p>All {@code ReadWriteLock} implementations must guarantee that
 * the memory synchronization effects of {@code writeLock} operations
 * (as specified in the {@link Lock} interface) also hold with respect
 * to the associated {@code readLock}. That is, a thread successfully
 * acquiring the read lock will see all updates made upon previous
 * release of the write lock.
 */

所有ReadWriteLock实现必须确保:writeLock操作的内存同步效应(依据Lock接口规范)同样适用于关联的readLock。即成功获取读锁的线程,能看到之前写锁释放时所做的所有更新。

 /* <p>A read-write lock allows for a greater level of concurrency in
 * accessing shared data than that permitted by a mutual exclusion lock.
 * It exploits the fact that while only a single thread at a time (a
 * <em>writer</em> thread) can modify the shared data, in many cases any
 * number of threads can concurrently read the data (hence <em>reader</em>
 * threads).
 * In theory, the increase in concurrency permitted by the use of a read-write
 * lock will lead to performance improvements over the use of a mutual
 * exclusion lock. In practice this increase in concurrency will only be fully
 * realized on a multi-processor, and then only if the access patterns for
 * the shared data are suitable.
 */

与互斥锁相比,读写锁在访问共享数据时允许更高程度的并发能力。其核心原理在于:虽然同一时间仅允许单个线程(写线程)修改共享数据,但在多数场景下,任意数量的线程可并发读取数据(即读线程)。

什么情况适合用ReadWriteLock

理论上,使用读写锁带来的并发性提升将优于互斥锁的性能表现;而实际应用中,这种并发性提升只有在多处理器环境下才能充分体现,且前提是共享数据的访问模式符合适用条件。

 /* <p>Whether or not a read-write lock will improve performance over the use
 * of a mutual exclusion lock depends on the frequency that the data is
 * read compared to being modified, the duration of the read and write
 * operations, and the contention for the data - that is, the number of
 * threads that will try to read or write the data at the same time.
 * For example, a collection that is initially populated with data and
 * thereafter infrequently modified, while being frequently searched
 * (such as a directory of some kind) is an ideal candidate for the use of
 * a read-write lock. However, if updates become frequent then the data
 * spends most of its time being exclusively locked and there is little, if any
 * increase in concurrency. Further, if the read operations are too short
 * the overhead of the read-write lock implementation (which is inherently
 * more complex than a mutual exclusion lock) can dominate the execution
 * cost, particularly as many read-write lock implementations still serialize
 * all threads through a small section of code. Ultimately, only profiling
 * and measurement will establish whether the use of a read-write lock is
 * suitable for your application.
 */

**相较于互斥锁,读写锁能否提升性能取决于三个因素:数据读取与修改的频率比、读写操作的持续时间、以及数据争用强度——即并发尝试读写数据的线程数量。**例如:一个初始化后频繁查询但极少修改的数据集合(如某类目录系统),就是读写锁的理想应用场景。但若更新操作变得频繁,数据将长期处于独占锁定状态,此时并发性提升微乎其微。更进一步,若读操作耗时过短,读写锁实现的开销(其固有复杂度高于互斥锁)可能反超执行成本,尤其因多数读写锁实现仍通过小段代码串行化所有线程。最终,唯有通过性能剖析和数据测量才能判定读写锁是否适用于当前应用。

 /* <p>Although the basic operation of a read-write lock is straight-forward,
 * there are many policy decisions that an implementation must make, which
 * may affect the effectiveness of the read-write lock in a given application.
 * Examples of these policies include:
 * <ul>
 * <li>Determining whether to grant the read lock or the write lock, when
 * both readers and writers are waiting, at the time that a writer releases
 * the write lock. Writer preference is common, as writes are expected to be
 * short and infrequent. Reader preference is less common as it can lead to
 * lengthy delays for a write if the readers are frequent and long-lived as
 * expected. Fair, or &quot;in-order&quot; implementations are also possible.
 *
 * <li>Determining whether readers that request the read lock while a
 * reader is active and a writer is waiting, are granted the read lock.
 * Preference to the reader can delay the writer indefinitely, while
 * preference to the writer can reduce the potential for concurrency.
 *
 * <li>Determining whether the locks are reentrant: can a thread with the
 * write lock reacquire it? Can it acquire a read lock while holding the
 * write lock? Is the read lock itself reentrant?
 *
 * <li>Can the write lock be downgraded to a read lock without allowing
 * an intervening writer? Can a read lock be upgraded to a write lock,
 * in preference to other waiting readers or writers?
 *
 * </ul>
 */

尽管读写锁基础操作简单直接,但实现时需制定多项策略决策,这些策略将直接影响读写锁在特定场景中的效能。典型策略包括:

1、当读写线程同时在写锁释放时等待,决策授予读锁还是写锁:

(1)写者优先(常见):因预期写操作耗时短且频率低

(2)读者优先(较少用):当读操作如预期般频繁且持久时,可能导致写操作长时间延迟

(3)公平排序:也可采用"先进先出"的实现策略

2、当存在活跃读线程且写线程等待时,决定是否授予新请求的读锁:读者优先策略可能无限期延迟写者执行,而写者优先策略则可能削弱并发潜力。

3、判定锁的可重入性:持有写锁的线程能否重入该写锁?持有写锁时能否再获取读锁?读锁本身是否可重入?

4、写锁能否在不允许其他写者介入的情况下降级为读锁?读锁能否优先于其他等待的读写线程升级为写锁?

综上所属,官方对于使用读写锁是否能带来并发性上的提升是不确定的,受很多因素的制约,所以在评估ReadWriteLock方案是否适用于您的应用时,应全面考量上述所有因素。

ReadWriteLock API

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

jdk里面的接口的定义都非常简约(单一职责原则),ReadWriteLock里面只声明了两个获取读锁和写锁的方法。readLock()方法返回读锁,writeLock()方法返回写锁。而读写锁的具体实现则放在ReadWriteLock接口的实现类中。下面我们来分析下读写锁的具体实现。

ReentrantReadWriteLock

读写锁的定义

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
    private static final long serialVersionUID = -6992448646407690164L;
    /** Inner class providing readlock */
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /** Inner class providing writelock */
    private final ReentrantReadWriteLock.WriteLock writerLock;
    ......
        
    /**
     * Creates a new {@code ReentrantReadWriteLock} with
     * the given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
    public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }        
}

ReentrantReadWriteLock是ReadWriteLock接口的具体实现类,并实现了ReadWriteLock接口的readLock()方法和writeLock()方法。其内部通过内部类的形式定义了读锁和写锁。

public static class WriteLock implements Lock, java.io.Serializable {
	...
    //lock()
    //tryLock()
    //unlock()    
    //newCondition()  
   	...    
}

public static class ReadLock implements Lock,java.io.Serializable {
	...
    //lock()
    //tryLock()
    //unlock()    
    //newCondition() {throw new UnsupportedOperationException()} 
   	...   
}

两种锁都实现了Lock接口,核心的API方法大致相同,都是基本的上锁、解锁相关方法,在此不详细展开分析了。不同的一点是WriteLock 支持获取 Condition 条件变量,ReadLock 不支持获取 Condition,尝试获取会抛UnsupportedOperationException。

读写锁的特性

共享与互斥

ReentrantReadWriteLock具有"读读共享,读写互斥,写写互斥"的特性,也就是获取读锁,若写锁被其他线程持有则阻塞;获取写锁,若读锁或写锁被其他线程持有则阻塞。

重入性‌

读锁和写锁均支持重入。写锁可重入获取读锁(锁降级),但读锁不能升级为写锁。

公平性‌

支持公平/非公平模式(默认非公平)。非公平模式下写锁优先获取,避免写线程饥饿。

锁降级‌

允许从写锁降级为读锁(先获取写锁→再获取读锁→释放写锁)。

ReentrantReadWriteLock内部还定义一些监控读写锁状态的相关API,如获取当前读锁持有数getReadLockCount(),检查写锁是否被持有isWriteLocked()等方法。

读操作为什么要加锁

在这里你可能会有一个问题:都是读数据为什么还要加读锁,加了锁不是降低了并发读的能力吗?其实不然,读缓存仍需加锁的主要原因如下:

防止脏读‌

缓存场景中虽然是读多写少,但仍会有写的时候,当写操作正在更新缓存时,读操作如果不加锁可能读取到未完全更新的中间状态数据,导致脏读。读操作加锁可在读期间阻塞写请求,保证数据一致性。

避免缓存击穿‌

高并发场景下,若读取过程遇到缓存失效的情况,多个线程可能同时穿透缓存直接访问数据库。通过加锁控制,仅允许一个线程触发数据加载,其余线程等待并复用加载结果,减少数据库压力。

锁降级需求‌

在写锁持有期间,若需保持数据可见性,可先获取读锁再释放写锁(锁降级)。此时读锁的作用是防止其他写操作干扰,确保降级过程中的数据安全。

热点数据竞争‌

对高频访问的热点数据,即使无写操作,不加锁可能导致大量线程争抢资源,引发性能抖动。适当的读锁可平衡并发与效率。

经典用法

竟然读写锁在读多写少的场景下显著提升并发性能,那么缓存场景再适合不过了,也是读写锁的经典应用场景。下面是读写锁在缓存中的使用以及锁降级的示例代码:

package com.demo;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class CacheWithReadWriteLock {
    private final Map<String, Object> cache = new HashMap<>();
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    public Object getData(String key) {
        // 1. 先尝试获取读锁
        rwLock.readLock().lock();
        try {
            Object data = cache.get(key);
            if (data != null) {
                // 缓存命中直接返回
                return data;
            }
        } finally {
            //释放读锁
            rwLock.readLock().unlock();
        }

        // 2. 缓存未命中,获取写锁
        rwLock.writeLock().lock();
        try {
            // 双重检查
            Object data = cache.get(key);
            if (data == null) {
                // 从数据库加载
                data = loadFromDB(key);
                cache.put(key, data);
            }
            // 3. 锁降级:获取读锁后释放写锁
            rwLock.readLock().lock();
            return data;
        } finally {
            // 释放写锁,保持读锁
            rwLock.writeLock().unlock();
        }
    }

    private Object loadFromDB(String key) {
        /***
         * 从数据库查询数据
         * ....
         */
        return "Data_" + key;
    }
}

实现要点说明:

  1. 读锁优先检查‌:先通过读锁快速检查缓存是否存在,避免一上来就竞争写锁。
  2. ‌写锁独占加载:当缓存失效时,通过写锁保证只有一个线程执行数据库查询。
  3. ‌锁降级机制:在写锁持有期间获取读锁,确保数据可见性后再释放写锁。
  4. ‌双重检查:避免多个写线程同时通过第一层检查后重复加载数据。

该方案相比互斥锁(如synchronized)的优势在于:

  • 读操作完全并行(无锁竞争)
  • 写操作仍保持线程安全
  • 避免缓存失效时的数据库雪崩效应

总结

本文由ReentrantLock独占的特性可能带来的性能问题,引出读写锁的概念,接着对读写锁的使用场景进行了分析,官方对于使用读写锁是否能带来并发性上的提升是不确定的,受很多因素的制约,倡导开发者根据实际情况合理评估使用。最后对读写锁的核心的API、关键特性、经典使用场景进行讲解,借此对读写锁有了一个比较全面的了解。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

帧栈

您的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值