一篇文章彻底搞懂AQS之ReentrantReadWriteLock(读写锁,深度剖析)


前言


一、ReentrantReadWriteLock介绍

现实里有一种场景:对共享资源的读和写的操作,且写操作没有读操作那么频繁(读多写少)。
在没有写操作的时候,可以多个线程并发去读一个共享资源而没有线程安全问题,所以应该允许多个线程同时读取共享资源(读读可以并发);
但是一个线程想去写共享资源,就不应该允许其他线程对该资源进行读或者写操作了。(读写、写写互斥)。

针对这种常见,JAVA的并发包提供了读写锁ReentrantReadWriteLock
他内部维护了一个读锁和一个写锁写锁是独占的,读锁是共享的

进入读锁的前提条件:

没有其他线程的写锁;
没有写请求,或者有写请求,但是调用线程和持有锁线程是同一个。

进入写锁的前提条件:

没有其他线程的读锁
没有其他线程的写锁

读写锁特性

公平性选择:支持 非公平(默认) 和公平的锁获取方式,吞吐量还是非公平优先
可重入: 读和写锁都支持重入。
读锁线程获取读锁后,还能够获取读锁。
写锁在获取线程后,还能获取写锁,同时也可以获取读锁。
锁降级: 获取写锁时,然后获取读锁,然后释放写锁的顺序,写锁能降级为读锁。(提高并发性能)。

PS:
该锁最多支持65535个递归写锁和65535个读锁。试图超过这些限制会导致锁定方法抛出Error。作者:Doug Lea


1、构造方法

创建了sync 公平、非公平的锁实例;
创建了一个读锁和一个写锁实例;


    public ReentrantReadWriteLock() {
        this(false);
    }
    
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

2、核心属性

/** 读锁 */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** 写锁 */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** 同步锁实例 */
final Sync sync;

3、核心的方法使用

private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock r = readWriteLock.readLock();
private Lock w = readWriteLock.writeLock();

// 读操作上读锁
public Data get(String key) {
  r.lock();
  try { 
      // TODO 业务逻辑
  }finally { 
       r.unlock(); 
   }
}

// 写操作上写锁
public Data put(String key, Data value) {
  w.lock();
  try { 
      // TODO 业务逻辑
  }finally { 
       w.unlock(); 
   }

PS:
读锁不支持条件变量
重入时升级不支持:持有读锁的情况下去获取写锁,会导致获取永久等待
重入时支持降级: 持有写锁的情况下可以去获取读锁


4、使用场景

ReentrantReadWriteLock适合读多写少的场景

示例Demo

public class Cache {
    static Map<String, Object> map = new HashMap<String, Object>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock r = rwl.readLock();
    static Lock w = rwl.writeLock();

    // 获取一个key对应的value
    public static final Object get(String key) {
        r.lock();
        try {
            return map.get(key);
        } finally {
            r.unlock();
        }
    }

    // 设置key对应的value,并返回旧的value
    public static final Object put(String key, Object value) {
        w.lock();
        try {
            return map.put(key, value);
        } finally {
            w.unlock();
        }
    }

    // 清空所有的内容
    public static final void clear() {
        w.lock();
        try {
            map.clear();
        } finally {
            w.unlock();
        }
    }

上述示例中,Cache组合一个非线程安全的HashMap作为缓存的实现,同时使用读写锁的读锁和写锁来保证Cache是线程安全的。在读操作get(String key)方法中,需要获取读锁,这使得并发访问该方法时不会被阻塞。写操作put(String key,Object value)方法和clear()方法,在更新 HashMap时必须提前获取写锁,当获取写锁后,其他线程对于读锁和写锁的获取均被阻塞,而 只有写锁被释放之后,其他读写操作才能继续。Cache使用读写锁提升读操作的并发性,也保证每次写操作对所有的读写操作的可见性,同时简化了编程方式


5、锁降级

锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。锁降级可以帮助我们拿到当前线程修改后的结果而不被其他线程所破坏,防止更新丢失

锁降级的使用示例
因为数据不常变化,所以多个线程可以并发地进行数据处理,当数据变更后,如果当前线程感知到数据变化,则进行数据的准备工作,同时其他处理线程被阻塞,直到当前线程完成数据的准备工作。

private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
private volatile boolean update = false;

public void processData() {
    readLock.lock();
    if (!update) {
        // 必须先释放读锁
        readLock.unlock();
        // 锁降级从写锁获取到开始
        writeLock.lock();
        try {
            if (!update) {
                // TODO 准备数据的流程(略)  
                update = true;
            }
            readLock.lock();
        } finally {
            writeLock.unlock();
        }
        // 锁降级完成,写锁降级为读锁
    }
    try {
        //TODO  使用数据的流程(略)
    } finally {
        readLock.unlock();
    }
}

锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性
如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新
如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。

重点:RentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程)。
目的也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的


二、源码分析

有目的性的从3个方向来看源码

  1. 读写锁是怎样实现分别记录读写状态的?
  2. 写锁是怎样获取和释放的?
  3. 读锁是怎样获取和释放的?

2.1、ReentrantReadWriteLock结构

在这里插入图片描述

在这里插入图片描述


2.2、读写状态的设计-(精髓所在)

设计的精髓:用一个变量如何维护多种状态

在ReentrantLock中,使用Sync(实际就是AQS)的int类型的state来表示同步状态,表示锁被一个线程重复获取的次数。
但是读写锁ReentrantReadWriteLock内部维护着一对读写锁,如果一个变量维护多种状态,就需要采用按位切割的方式来维护这个变量,
在这里被切割成2部分,高16位表示读,低16位表示写。

同步状态如图:
在这里插入图片描述

代码实现:java.util.concurrent.locks.ReentrantReadWriteLock.Sync

static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

exclusiveCount(int c) 静态方法,获得持有写状态的锁的次数
sharedCount(int c) 静态方法,获得持有读状态的锁的线程数量。不同于写锁,读锁可以同时被多个线程持有。
每个线程持有的读锁支持重入的特性,所以需要对每个线程持有的读锁的数量单独计数,这就需要用到 HoldCounter 计数器


2.3、HoldCounter 计数器

用于每个线程读取保持计数的计数器。作为ThreadLocal维护(如果不熟悉ThreadLocal可以看我上一篇文章);
读锁的内在机制就是一个共享锁,一次共享锁的操作相当于对HoldCounter 计数器的操作。
获取共享锁,则该计数器+1,释放共享锁则-1;
只有当线程获取共享锁后才能对共享锁进行释放、重入操作。

static final class HoldCounter {
            int count = 0;
            final long tid = getThreadId(Thread.currentThread());
        }

        static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }

HoldCounter : 是用来记录读锁重入数的对象
ThreadLocalHoldCounter :是ThreadLocal变量,用来存放不是第一个获取读锁,的线程的其他线程的读锁重入数对象


2.4、写锁的获取逻辑

写锁是一个支持重入的排它锁;
如果当前线程已经获取了写锁,则更新写状态;
如果当前线程已经获取了读锁、或者不是重入的线程,则阻塞等待;

这里的写锁逻辑是重写了AQS的tryAcquire方法
在这里插入图片描述

protected final boolean tryAcquire(int acquires) {
    //当前线程
    Thread current = Thread.currentThread();
    //获取state状态   存在读锁或者写锁,状态就不为0
    int c = getState();
    //获取写锁的重入数
    int w = exclusiveCount(c);
    //当前同步状态state != 0,说明已经有其他线程获取了读锁或写锁
    if (c != 0) {
        // c!=0 && w==0 表示存在读锁
        // 当前存在读锁或者写锁已经被其他写线程获取,则写锁获取失败
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        // 超出最大范围  65535
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        //同步state状态
        setState(c + acquires);
        return true;
    }
    // writerShouldBlock有公平与非公平的实现, 非公平返回false,会尝试通过cas加锁
    //c==0 写锁未被任何线程获取,当前线程是否阻塞或者cas尝试获取锁 
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;

    //设置写锁为当前线程所有
    setExclusiveOwnerThread(current);
    return true;
}

通过源码我们可以知道:
读写互斥
写写互斥
写锁支持同一个线程重入


获取锁失败后,就是执行AQS的经典独占锁入队逻辑(addWaiter、enq、acquireQueued,因前面AQS文章已经分析过,这里就不在重复);

获取写锁逻辑图
在这里插入图片描述


2.5、写锁的释放逻辑

这里看源码可以看到,写锁的释放的逻辑也是重写了
在这里插入图片描述

protected final boolean tryRelease(int releases) {
    //若锁的持有者不是当前线程,抛出异常
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;
    //当前写状态是否为0,为0则释放写锁
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}

写锁的释放逻辑很简单
1、就是看解锁的线程是不是当前持有锁的线程;
2、状态-1后状态是否为0,为0就把持有锁线程设置空,不为0,结果返回false,表示还没释放掉(重入锁还没释放完);
3、更新状态

在这里插入图片描述


2.6、读锁的获取逻辑

读锁是一个共享锁;
通过重写AQS的tryAcquireShared方法来实现;
在这里插入图片描述

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    // 如果写锁已经被获取并且获取写锁的线程不是当前线程,当前线程获取读锁失败返回-1   判断锁降级
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    //计算出读锁的数量
    int r = sharedCount(c);
    /**
    * 读锁是否阻塞    readerShouldBlock()公平与非公平的实现
    * r < MAX_COUNT: 持有读锁的线程小于最大数(65535)
    *  compareAndSetState(c, c + SHARED_UNIT) cas设置获取读锁线程的数量
    */
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {   //当前线程获取读锁
        
        if (r == 0) {  //设置第一个获取读锁的线程
            firstReader = current; 
            firstReaderHoldCount = 1;  //设置第一个获取读锁线程的重入数
        } else if (firstReader == current) { // 表示第一个获取读锁的线程重入
            firstReaderHoldCount++;
        } else { // 非第一个获取读锁的线程
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;  //记录其他获取读锁的线程的重入次数
        }
        return 1;
    }
    // 尝试通过自旋的方式获取读锁,实现了重入逻辑
    return fullTryAcquireShared(current);
}

PS:
读锁共享,读读不互斥
读锁可重入,每个获取读锁的线程都会记录对应的重入数
读写互斥,锁降级场景除外
支持锁降级,持有写锁的线程,可以获取读锁,但是后续要记得把读锁和写锁读释放
readerShouldBlock读锁是否阻塞实现取决公平与非公平的策略(FairSync和NonfairSync)

在这里插入图片描述


2.7、读锁的释放逻辑

读锁的释放,重写了tryReleaseShared方法;
在这里插入图片描述

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    //如果当前线程是第一个获取读锁的线程
    if (firstReader == current) {
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--; //重入次数减1
    } else {  //不是第一个获取读锁的线程
        HoldCounter rh = cachedHoldCounter;  
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        --rh.count;  //重入次数减1
    }
    for (;;) {  //cas更新同步状态
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            // Releasing the read lock has no effect on readers,
            // but it may allow waiting writers to proceed if
            // both read and write locks are now free.
            return nextc == 0;
    }

在这里插入图片描述


总结

ReentrantReadWriteLock是一个读写锁,内部有一个读锁(共享锁),和一个写锁(独占锁),支持锁降级特性。
适合读多写少的场景;
这里需要好好关注的是 一个状态来实现读写锁状态的设计,也是精髓所在。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

未闻花名丶丶

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

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

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

打赏作者

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

抵扣说明:

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

余额充值