读写锁-ReentrantReadWriteLock源码分析与图解

简介: 读写锁-ReentrantReadWriteLock源码分析与图解

为何要有读写锁

ReentrantLock锁和其他锁基本上都是排他锁,排他锁指的是在同一时刻只允许一个线程持有锁,访问共享资源。虽然ReentrantLock支持同一个线程重入,但是允许重入的是同一个线程。因此ReentrantReadWriteLock是为了支持多个线程在同一个时刻对于锁的访问孕育而生的。


读写锁—简单介绍

读写锁ReentrantReadWriteLock允许多个线程在同一时刻对于锁的访问。但是,写线程获取写锁时,所有的读线程和其他写线程均会被阻塞。读写锁是通过维护一对锁(一个读锁、一个写锁)来实现的,读写锁在很多读多于写得场景中有很大的性能提升。举个例子,当我们对数据库中的一条记录进行读取时,不应该阻塞其他读取这条数据的线程,而如果是有线程对该记录进行写操作,数据库就应该阻止其他线程对这条数据的读取和写入,这种场景就可以用类似读写锁的方式来处理。


特性

image.png使用示例

通过ReentrantReadWriteLock来实现一个线程安全的简单内存缓存设计,通过给缓存获取get(String key)方法加上读锁,允许其他线程在同一个时刻进行数据读取;给put(String key,String value)方法加上写锁,禁止在对缓存写入的时候其他线程对于缓存的读取和写入操作。

package com.lizba.p6;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
 * <p>
 *      使用ReentrantReadWriteLock实现一个简单的线程安全基于map的缓存设计
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/6/22 22:11
 */
public class CacheDemo {
    /** 存储数据容器 */
    private static Map<String, Object> cache = new HashMap<>();
    /** 读写锁ReentrantReadWriteLock */
    private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    /** 读锁 */
    static Lock readLock = lock.readLock();
    /** 写锁 */
    static Lock writeLock = lock.writeLock();
    /**
     * 获取数据,使用读锁加锁
     * @param key
     * @return
     */
    public static Object get(String key) {
        readLock.lock();
        try {
            return cache.get(key);
        } finally {
            readLock.unlock();
        }
    }
    /**
     * 设置key&value,使用写锁,禁止其他线程的读取和写入
     * @param key
     * @param value
     * @return
     */
    public static void put(String key, Object value) {
        writeLock.lock();
        try {
            cache.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }
    /**
     * 清空缓存
     */
    public static void flush() {
        writeLock.lock();
        try {
            cache.clear();
        } finally {
            writeLock.unlock();
        }
    }
}

实现分析

ReentrantReadWriteLock是基于AQS来实现的,我们知道AQS是以单个int类型的原子变量来表示其状态的,那么一个状态如何即表示读锁又表示写锁呢?观察源码发现(后续会有源码解析)ReentrantReadWriteLock通过使用一个32位的整型变量的高16位表示读,低16位表示写来维护读线程和写线程对于锁的获取情况。此时部分读者可能又会产生一个疑问,写锁是互斥的,只支持有一个线程持有写锁,用低16位记录同一个线程对写锁的多次重入是没问题的;那么对于允许多个线程获取的读锁,高16位又是如何维护持有锁的线程数和单个线程对读锁的获取次数的呢?其实这里引入了ThreadLocal来记录单个线程自己对于读锁的获取次数,高16位存储的是获取读锁的线程的个数。这里只是简单说下疑问,后续源码会有详细分析。我们先来看看这个int类型的变量的划分方式。

image.png如上个这个图写状态=1,表示当前线程获取了写锁,读状态=2,表示该线程同时获取了两次读锁。使用高低位来表示读写状态,那么状态的获取和设值是如何实现的呢?



写状态的get()


此时假设同步状态,也就是32的整型变量的值为S,写状态的获取方式为S&0x0000FFFF,这种办法会保留低16位的同时抹去高16位,也就能计算出写状态的获取情况。

image.pngimage.png源码分析

源码分析主要分为四块,分别是ReentrantReadWriteLock中一个int分为两半计算方式、写锁的获取与释放、读锁的获取与释放以及锁降级这几个方面来展开。


1、一个int分为两半计算方式

int拆分为高16位和低16位的计算是在ReentrantReadWriteLock的内部类在Sync中实现的,其主要核心如下image.png

2、写锁的获取与释放

WriteLock写锁是一个支持重入的排它锁,它的获取与释放通过tryAcquire(int arg)和tryRelease(int arg)来实现,获取到写锁的条件是,当前读锁未被获取或者当前线程是持有写锁的线程,否则获取失败进入等待状态。

写锁获取image.pngimage.png3、读锁的获取与释放


读锁的获取与释放相比写锁的获取与释放相对来说要复杂一些,因为它是支持重入的共享锁,它能被多个线程同时获取,因此我们不仅需要记录获取读锁的每一个线程,同时需要记录每个线程对于读锁的重入次数,因此我们首先来看读锁的重入计数。


读锁的重入计数

读锁的重入计数,巧妙的使用每个线程自己来记录,通过存在在ThreadLocal中的HoldCounter中的变量count来增加和减少,其是线程隔离的因此也是线程安全的,具体实现在Sync中。

image.png读锁获取

 /*
  * tryAcquireShared方法实现在Sync中,unused如其定义未被使用
  */
protected final int tryAcquireShared(int unused) {
  // 获取当前线程
    Thread current = Thread.currentThread();
    // 获取同步状态值
    int c = getState();
    // exclusiveCount(c)获取写锁的值,如果不为0表示写锁已被持有
    // getExclusiveOwnerThread(),如果是写锁则需要判断当前线程和持有写锁(互斥锁)的线程是否相等
    // 因为写锁被允许获取读锁,如果不是同一个线程则直接返回-1
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    // 获取读锁持有的线程数
    int r = sharedCount(c);
    // readerShouldBlock() 有两个实现分别是FairSync和NonfairSync,两种的获取锁策略实现不一样,用于判断是否获取读锁
    // r < MAX_COUNT 判断读锁获取线程数是否小于最大值
    // compareAndSetState(c, c + SHARED_UNIT) 尝试获取读锁
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        // 如果获取所成功
        // 当前线程为第一个获取锁的线程,并且是第一次
        if (r == 0) {
            // 设置当前线程为firstReader
            firstReader = current;
            // 设置当前线程重入读锁的次数为1
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {  // 如果不是第一次,但是是第一个获取读锁的线程
            // 当前线程重入读锁的次数为累加1
            firstReaderHoldCount++;
        } else {
            // 获取缓存cachedHoldCounter
            HoldCounter rh = cachedHoldCounter;
            // 如果缓存为空,或者缓存的线程ID与当前线程ID不一致
            if (rh == null || rh.tid != getThreadId(current))
                // 从ThreadLocalHoldCounter中读取当前线程的读锁重入次数,设置缓存
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0) // 缓存存在,但是重入次数为空
                // 设置当前线程readHolds中的HoldCounter
                readHolds.set(rh);
            // 重入次数+1
            rh.count++;
        }
        return 1;
    }
    // 获取读锁失败,自旋重试
    return fullTryAcquireShared(current);
}

image.pngimage.png

目录
相关文章
|
设计模式 Java API
StampedLock源码分析(2)
StampedLock也提供了单独读锁和写锁的封装类WriteLockView和ReadLockView,它俩存在的意义就是只讲锁的部分暴露出去,防止外部接口错误加解锁,我觉得符合软件设计模式中的单一职责和接口隔离原则。
66 1
【ReentrantReadWriteLock的实现原理】
【ReentrantReadWriteLock的实现原理】
|
Java API
StampedLock源码分析(1)
之前已经说过了ReentrantLock ReentrantReadWriteLock,可以参考之前的博客。在ReentrantReadWriteLock源码解析文末,我提到了ReentrantReadWriteLock的缺点,就是无法避免写线程饥渴的问题,而今天要说的StampedLock提供了乐观读的API,解决了写饥渴的问题。
57 0
|
7月前
|
存储 Java
StampedLock(戳记锁)源码解读与使用
StampedLock(戳记锁)源码解读与使用
|
7月前
|
安全 Java 测试技术
ReentrantReadWriteLock(可重入读写锁)源码解读与使用
ReentrantReadWriteLock(可重入读写锁)源码解读与使用
|
7月前
|
安全 Java
ReentrantLock 原理你都知道吗?
通过以上步骤和示例代码,你应该对 ReentrantLock 的工作原理有了清晰的理解。欢迎关注威哥爱编程,一起学习成长。
图解ReentrantLock底层公平锁和非公平锁实现原理
图解ReentrantLock底层公平锁和非公平锁实现原理
186 0
图解ReentrantReadWriteLock读写锁的实现原理(下)
图解ReentrantReadWriteLock读写锁的实现原理
126 0
图解ReentrantReadWriteLock读写锁的实现原理(下)
|
Java API
图解ReentrantReadWriteLock读写锁的实现原理(上)
图解ReentrantReadWriteLock读写锁的实现原理
208 0
图解ReentrantReadWriteLock读写锁的实现原理(上)