ReentrantReadWriteLock的自我认知篇

ReentrantReadWriteLock

ReadWriteLock

ReadWriteLock 是一个接口,ReentrantReadWriteLock 实现了该接口的方法,除了 ReentrantReadWriteLock 之外,还有一个 StampedLock(邮戳锁) 的实现类

public interface ReadWriteLock {
    // 读锁
    Lock readLock();
    // 写锁
    Lock writeLock();
}

实际上重写的两个方法是获取 writeLock 和 readLock

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

ReentrantReadWriteLock

构造方法

默认的无参构造方法使用的是非公平锁

public ReentrantReadWriteLock() {
    this(false);
}

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

成员变量

主要就是这三个,定义了读锁,写锁,还有 Sync 内部类

private final ReentrantReadWriteLock.ReadLock readerLock;

private final ReentrantReadWriteLock.WriteLock writerLock;

final Sync sync;

成员方法

// 判断该 ReentrantReadWriteLock 锁是否是公平锁
public final boolean isFair() {
    return sync instanceof FairSync;
}

// 获取读锁的线程总数
public int getReadLockCount() {
    return sync.getReadLockCount();
}

// 判断该 ReentrantReadWriteLock 是否是写锁
public boolean isWriteLocked() {
    return sync.isWriteLocked();
}

// 获取线程写锁的可重入次数
public int getWriteHoldCount() {
    return sync.getWriteHoldCount();
}

// 获取线程读锁的可重入次数
public int getReadHoldCount() {
    return sync.getReadHoldCount();
}

// 获取同步等待队列中写锁的线程
protected Collection<Thread> getQueuedWriterThreads() {
    return sync.getExclusiveQueuedThreads();
}

// 获取同步等待队列中读锁的线程
protected Collection<Thread> getQueuedReaderThreads() {
    return sync.getSharedQueuedThreads();
}

// 判断是否存在同步等待队列,即表示是否有多线程资源抢夺的情况
public final boolean hasQueuedThreads() {
    return sync.hasQueuedThreads();
}

// 判断指定线程是否在同步等待队列中
public final boolean hasQueuedThread(Thread thread) {
    return sync.isQueued(thread);
}

...
...

Sync

底层也是继承于 AQS,实现了 AQS 中的模板方法

成员属性

SHARED_SHIFT、SHARED_UNIT、EXCLUSIVE_MASK、MAX_COUNT

ReentrantReadWriteLock 为了提高效率节省资源,state 状态值包括了 读状态、写状态,所以需要作区分,因此使用高低位切割实现state状态变量维护两种状态,即16位表示读状态,低16位表示写状态。

在这里插入图片描述

// 偏移位数
static final int SHARED_SHIFT   = 16;

// 1向左移16位,高16位
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);

// 1向左移16位减1,最大的可重入数量
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;

// 1向左移16位减1,低16位
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
高16位表示读状态

读锁的初始状态值 SHARED_UNIT 为:0000 0000 0000 0001 0000 0000 0000 0000,如果遇到多线程获取读锁的时候,计算方式是 compareAndSetState(c, c + SHARED_UNIT),每次都加上初始化读锁的状态值,从而获取结果;读锁的 SHARED_UNIT 在计算重入数和判断是否符合可重入的条件都起到了作用

// 1向左移16位,高16位
static final int SHARED_UNIT = (1 << SHARED_SHIFT);

在这里插入图片描述

低16位表示写状态

写锁初始的状态值 EXCLUSIVE_MASK 为:0000 0000 0000 0000 1111 1111 1111 1111,如果遇到多线程获取读锁的时候,计算方式是 compareAndSetState(c, c + acquires),每次都加上重入次数,从而获取结果;EXCLUSIVE_MASK 只在判断是否符合可重入条件中起作用

// 1向左移16位减1,低16位
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

在这里插入图片描述

HoldCounter

HoldCounter是用来记录线程可重入次数的对象

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

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

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();
    }
}

构造方法

很明显,在这一操作是初始化 ThreadLocalHoldCounter,然后设置 state 状态的初始化值为 0

Sync() {
    readHolds = new ThreadLocalHoldCounter();
    setState(getState());
}

成员方法

sharedCount(int) : int, 获取读锁的重入次数

exclusiveCount(int): int, 获取写锁的重入次数

readerShouldBlock(): boolean, 判断读操作是否需要阻塞,这是通过调用的是公平 / 非公平锁来决定的,若是非公平锁,那么读操作就会根据CLH中的同步队列是否有独占线程,若有则读阻塞;若是公平锁,那么会判断CLH同步队列中是否有其他线程,若有则读阻塞

wirterShouldBlock(): boolean, 判断写操作是否需要阻塞,这也是通过调用的是公平 / 非公平锁来决定的,若是非公平锁,那么写操作就会返回 false,因为写操作本身就是利用 AQS 的独占模式获取锁的,独占相当于阻塞;若是公平锁,那么会判断CLH同步队列中是否有其他线程,若有则读阻塞

tryRelease(int): boolean, 写锁释放锁

tryAcquire(int): boolean, 写锁获取锁

tryReleaseShared(int): boolean, 读锁释放锁

tryAcquireShared(int): boolean, 读锁获取锁

tryWriteLock(): boolean, 尝试通过写锁获取锁

tryRaadLock(): boolean, 尝试通过读锁获取锁

NofairSync

非公平锁,继承了父类 Sync,并且有两个方法判断读、写状态下是否会阻塞

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = -8159625535654395037L;
    final boolean writerShouldBlock() {
        return false;
    }
    final boolean readerShouldBlock() {
        return apparentlyFirstQueuedIsExclusive();
    }
}

// 这个方法就是去判断CLH队列中是否存在独占线程
final boolean apparentlyFirstQueuedIsExclusive() {
    Node h, s;
    return (h = head) != null &&
        (s = h.next)  != null &&
        !s.isShared()         &&
        s.thread != null;
}

fairSync

公平锁,继承了父类 Sync,并且有两个方法判断读、写状态下是否会阻塞

static final class FairSync extends Sync {
    private static final long serialVersionUID = -2274990926593161451L;
    final boolean writerShouldBlock() {
        return hasQueuedPredecessors();
    }
    final boolean readerShouldBlock() {
        return hasQueuedPredecessors();
    }
}

// 这个方法其实就是调用 AQS 的,能够判断 CLH 同步队列是否有其他线程,若有则 CLH 同步队列的线程先获取锁资源
public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}

ReadLock

读锁实现了 Lock 接口,里面的所有成员方法都是通过调用 Sync 的方法,所以也是依赖 Sync 的;除此之外,读锁是通过 AQS 的共享模式 Share 实现的,下面会有获取和释放读锁的具体流程

public static class ReadLock implements Lock, java.io.Serializable {
    
    private final Sync sync;

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

    public void lock() {
        sync.acquireShared(1);
    }

    public void lockInterruptibly() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

    public boolean tryLock() {
        return sync.tryReadLock();
    }

    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

    public void unlock() {
        sync.releaseShared(1);
    }

    public Condition newCondition() {
        throw new UnsupportedOperationException();
    }
}

WriteLock

写锁实现了 Lock 接口,里面的所有成员方法也都是通过调用 Sync 的方法,所以也是依赖 Sync 的;除此之外,写锁是通过 AQS 的独占模式 Exclusive 实现的

public static class WriteLock implements Lock, java.io.Serializable {

    private final Sync sync;

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

    public void lock() {
        sync.acquire(1);
    }

    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    public boolean tryLock( ) {
        return sync.tryWriteLock();
    }

    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }

    public void unlock() {
        sync.release(1);
    }

    public Condition newCondition() {
        return sync.newCondition();
    }
    
    public boolean isHeldByCurrentThread() {
        return sync.isHeldExclusively();
    }

    public int getHoldCount() {
        return sync.getWriteHoldCount();
    }
}

ReadLock 获取读锁流程

遵循 ReentrantReadWriteLock 接口规范,首先获取读锁需要调用 ReentrantReadWriteLock.readLock()

其次 ReadLock 有两种方法可以获取到读锁:tryLock() 非阻塞方法,可以立马返回是否成功获取锁的结果;而 lock() 方法是阻塞的直到当前线程成功获取到读锁为止,在代码方面 tryLock() 和 lock() 都是大同小异的

lock()

ReadLock 调用 lock() 方法,实际上是调用了 AQS 的 acquireShared(),然后又会回调 Sync 中的对 AQS 实现的模板方法 tryAcquireShared(),具体流程如下

代码
// ReadLock中的lock方法
public void lock() {
    sync.acquireShared(1);
}

// AQS中的acquireShared方法
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

// AQS中的模板方法tryAcquireShared,是在Sync中重写了
protected final int tryAcquireShared(int unused) {
    
    // 获取当前线程
    Thread current = Thread.currentThread();
    
    // 获取当前的state状态值
    int c = getState();
    
    // exclusiveCount() 方法是获取写锁的重入次数,如果不等于0说明有写锁,再判断持有写锁的线程是不是当前线程
    // 若是,则进行锁的降级(写锁降级为读锁,反之则不允许);若不是则尝试获取锁失败;如果等于0说明该线程没有写锁
    // 这时就可以获取读锁
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    
    // 获取读锁的可重入次数
    int r = sharedCount(c);
    
    // 判断读操作是否需要阻塞,这是根据初始化Sync时定义的是公平锁还是非公平锁来决定的
    // 非公平情况下,会判断CLH同步等待队列中是否有独占的线程,有则阻塞走 fullTryAcquireShared 方法
    // 无则以非阻塞的方式继续执行
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        
        // 获取成功,判断读锁的可重入次数是否为0
        if (r == 0) {

            // 将当前线程赋值到 firstReader,根据字面意思就是赋值给首次获取锁的线程
            firstReader = current;
            // 将 firstReaderHoldCount 赋值为1,表示首次获取读锁的重入次数为1
            firstReaderHoldCount = 1;

            // 判断当前线程 是否是 首次获取锁的线程,
        } else if (firstReader == current) {
            // 若是的话,firstReaderHoldCount++,重入次数加1
            firstReaderHoldCount++;

            // 若以上条件都不满足,表示是其他线程获取读锁
        } else {

            // 将每个不同的线程的可重入次数进行缓存,放入 cachedHoldCounter 中
            // readHolds 为 ThreadLocalHoldCounter 对象,这个类继承了 ThreadLocal,更加清楚的知道这里的逻辑
            // 主要就是将每个线程进行获取锁可重入次数的数据隔离
            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;
    }
    // 通过阻塞的方式去获取读锁,和tryAcquireShared非阻塞的方式很像,只不过使用了cas自旋获取锁
    return fullTryAcquireShared(current);
}
对应的流程图

在这里插入图片描述

调用链如下

在这里插入图片描述

从以上流程可以得出

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

ReadLock 释放读锁流程

ReadLock 中的释放锁是通过调用 unlock 方法执行的,获取到读锁,执行完临界区后,要记得释放读锁(如果重入多次要释放对应的次数),不然会阻塞其他线程的写操作

unlock()

代码
// ReadLock 中的 lock 方法
public void unlock() {
    sync.releaseShared(1);
}

// AQS 中的 releaseShared 方法
public final boolean  releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

// AQS 中的模板方法 tryReleaseShared,Sync 中已经对这个方法做了重写
protected final boolean tryReleaseShared(int unused) {
    
    // 获取当前线程
    Thread current = Thread.currentThread();
    
    // 判断是否是当前线程释放锁,若是则firstReaderHoldCount重入次数-1
    if (firstReader == current) {
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    // 若不是,则从ThreadLocal缓存中获取对应线程的可重入数,再减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;
    }
    // 再自旋释放当前线程的锁,返回 nextc == 0 主要是因为锁的可重入性,释放的时候必须全部释放
    // 只有全部的读锁被释放了,才会去执行doReleaseShared函数
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}
对应的流程图

在这里插入图片描述

调用链如下

在这里插入图片描述

WriteLock 获取写锁流程

WriteLock 也实现了 lock 接口,其次 WriteLock 也有两种方法可以获取到写锁:tryLock() 非阻塞方法,可以立马返回是否成功获取锁的结果;而 lock() 方法是阻塞的直到当前线程成功获取到读锁为止,在代码方面 tryLock() 和 lock() 都是大同小异的

lock()

WriteLock 调用 lock() 方法,实际上是调用了 AQS 的 acquire(),然后又会回调 Sync 中的对 AQS 实现的模板方法 tryAcquire(),具体流程如下

代码
// WriteLock 中的 lock 方法
public void lock() {
    sync.acquire(1);
}

// AQS 中的 acquire 方法
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

// AQS 中的模板方法 tryAcquire,在 WriteLock 中已经做了重写
protected final boolean tryAcquire(int acquires) {
    
    // 获取当前线程
    Thread current = Thread.currentThread();
    
    // 获取状态值 state,包括读锁和写锁
    int c = getState();
    
    // 计算写锁的可重入次数
    int w = exclusiveCount(c);
    
    // 若状态值不为0,即有写锁或者读锁的情况下
    if (c != 0) {
        
        // 判断如果没有写锁,或者当前线程也没有持有写锁的情况下,获取锁失败,因为如果c!=0,但是当前线程又没有写锁的情况下
        // 实际上是只有读锁,锁是不能够由读锁升级为写锁的
        // 这里能反映出 读写互斥 写写互斥
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        
        // 若写锁的可重入数大于最大重入次数,则抛异常
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        
        // 更新state
        setState(c + acquires);
        return true;
    }
    
    // 判断写操作是否阻塞,若阻塞,则获取锁失败;若不阻塞但是state状态值自旋更新失败,则获取锁也失败
    // 判断写操作是否阻塞是根据 Sync 定义的公平还是非公平决定的
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    
    // 获取到锁,就将当前线程设置为获取锁的线程
    setExclusiveOwnerThread(current);
    return true;
}
调用链如下

在这里插入图片描述

对应的流程图

在这里插入图片描述

从以上流程可以得出

  • 读写互斥
  • 写写互斥
  • 写锁支持同一个线程重入
  • writerShouldBlock写锁是否阻塞实现取决公平与非公平的策略(FairSync和NonfairSync)

WriteLock 释放写锁流程

获取到写锁,临界区执行完,要记得释放写锁(如果重入多次要释放对应的次数),不然会阻塞其他线程的读写操作,调用unlock函数释放写锁

unlock()

// WriteLock 的方法
public void unlock() {
    sync.release(1);
}

// AQS 的方法
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

// AQS 中的模板方法,在 WriteLock 进行了重写
protected final boolean tryRelease(int releases) {
    
    // 判断持有写锁的线程是不是当前线程,若不是则抛出异常
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    
    // state减1
    int nextc = getState() - releases;
    
    // 判断写锁的重入次数是否已经为0
    boolean free = exclusiveCount(nextc) == 0;
    
    // 若为0,则将持有写锁的线程设置为null
    if (free)
        setExclusiveOwnerThread(null);
    
    // 重新将state赋值
    setState(nextc);
    return free;
}
调用链如下

在这里插入图片描述

对应的流程图

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

99Savage

你的鼓励是我最大的动力!加油

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

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

打赏作者

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

抵扣说明:

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

余额充值