十、详解ReentrantReadWriteLock读写锁

写在前面

之前我们讲过可重入锁五、详解ReentrantLock-CSDN博客 从这篇博文中我们可以了解到,基于lock的锁底层都是利用aqs这个抽象类的。

那么在读写锁中,其本质也是利用aqs,与可重入锁之间的区别的就是在实现抽象方法的时候,具体的逻辑不一样。可以理解为两者骨架一样,但是具体细节逻辑有区别,尤其是在tryAcquire和tryRelease的逻辑不一样。

ReentrantReadWriteLock使用方式

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReentrantReadWriteLockDemo {
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private String message;

    public void writeMessage(String message) {
        lock.writeLock().lock(); //获取写锁,然后加锁
        try {
            System.out.println("Writing message: " + message);
            this.message = message;
        } finally {
            lock.writeLock().unlock();
        }
    }

    public void readMessage() {
        lock.readLock().lock(); //获取读锁,然后加锁
        try {
            System.out.println("Reading message: " + message);
        } finally {
            lock.readLock().unlock();
        }
    }

  
}

源码分析

首先从ReentrantReadWriteLock的构造函数入手,看看在构造函数中到底做了什么
 

 public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();//公平和非公平
        readerLock = new ReadLock(this); //创建读锁
        writerLock = new WriteLock(this); //创建写锁
}

可以看到第一步创建了sync,其实就是一个aqs。然后判断公平和非公平,这个我们在可重入锁的博文中讲过。

然后创建了读、写Lock对象。注意,这里这两个对象将this当做入参,我们细看一下这个两个对象到底是什么:

 protected ReadLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
}


protected WriteLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
}

可以看到ReadLock对象和WriteLock对象其实本质上就是对Sync对象的封装。但是这里需要注意的是,ReadLock和WriteLock对象引用的是同一个sync对象。因此这里得到一个结论,ReentrantReadWriteLock,ReadLock,WriteLock 都是针对同一个sync对象进行操作。

加读锁

下面我们看一下如何加读锁,核心函数为 ReadLock.lock():

public void lock() {
      sync.acquireShared(1); //获取共享。入参为1,表示只获取一个令牌
}


//这个就是aqs的模板方法,主要是约定流程
public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0) //开始尝试进行处理,具体逻辑在子类中
            doAcquireShared(arg); //如果加锁失败,那么就进入等待,内部和可重入一样
}

其核心在于tryAcquireShared是如何处理的,怎么才算加上了锁,怎么才算未加上锁:

@ReservedStackAccess
        protected final int tryAcquireShared(int unused) {
           
            Thread current = Thread.currentThread();
            int c = getState();
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            int r = sharedCount(c);
            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 != LockSupport.getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            return fullTryAcquireShared(current);
        }

尝试分析一下:
1、先获取当前锁状态,也就是获取state的值

2、通过exclusiveCount方法来获取是否有写锁。其原理就是state的高16位为读锁,低16位表示写锁。读锁被一个或多个线程持有时,state的高16位会增加;当读锁被释放时,高16位会减少。写锁被一个线程持有时,state的低16位会增加;当写锁被释放时,低16位会减少。

3、如果说写锁不是0,那么就以为这有线程加了写锁,然后判断加写锁的线程是否为自己,如果不是自己,那么加锁失败。这里注意,既然已经判断了写锁不是0,直接返回加读锁失败不就好了吗,为什么还需要判断加写锁的那个线程是否为自己呢??  这里涉及到锁升级:

        如果一个线程加了写锁,那么这个线程还可以加读锁。但是如果一个线程加了读锁,那么他不能加写锁。这个很好理解,因此写锁是共享的,一个线程加了读锁,那么同一个时刻还有其他线程也加了读锁,所以这个线程不能加写锁。但是反过来,如果一个线程加了写锁,那么一定就保证了当前只有一个线程获取到了锁,那么他就可以加读锁。

4、如果说没有写锁,或者说写锁是自己加的,那么利用cas进行加读锁,将高16位进行加1。

加写锁

加写锁的核心代码为Write.lock()方法。这方法底层调用的标准流程和ReentrantLock一样,都是调用aqs的模板方法acquire()。具体的区别就是如何实现tryAcquire()方法。

protected final boolean tryAcquire(int acquires) {
            
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
                // (Note: if c != 0 and w == 0 then shared count != 0)
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
}

尝试分析一下:
1、同样先获取到锁状态。然后计算出写锁的状态。

2、如果说当前有线程加锁:

        如果w==0,意味当前有线程加了读锁,那么直接失败。

        如果w!=0,意味着当前有线程加了写锁,但是【锁持有】线程不是当前线程,那么也失败

        如果有线程加了写锁,且是自己加的,这里还有一个最大限制,然后直接设置锁状态,无需cas。返回加锁成功

3、如果没有线程加锁:

        判断当前线程是否应该等待(这个与公平锁和非公平锁有关),如果等待,直接加锁失败,如果不等待尝试cas,如果cas失败,那么就直接加锁失败,如果成功,那么就将【锁持有】线程设置为自己。这个有点类似于ReentrantLock。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值