【Java 并发】【九】【AQS】【八】ReentrantReadWriteLock之WriteLock写锁原理

发布时间 2023-04-07 23:30:43作者: 酷酷-

1  前言

上节我们看了下ReentrantReadWriteLock读写锁的属性以及内部结构,我们回顾下:
(1)ReentrantReadWriteLock内部有两把锁,读锁ReadLock、写锁WriteLock,基于AQS实现的读写锁并发工具Sync;其中无论读锁还是写锁都是基于Sync进行封装的。
(2)讲解了ReentrantReadWriteLock内部类的体系结构,有公平锁FairSync、非公平锁NonfairSync,它两都继承自Sync,而Sync又继承自AQS,所以基于AQS的机制实现的锁机制。
(3)讲解了读锁、写锁分别是怎么表示的,使用一个int类型32位的数字同时表示读锁和写锁,高16位表示读锁、低16位表示写锁。
那么这节我们就先来看看写锁WriteLock,看看人家底层是怎么实现写锁的。

2  写锁源码分析

我们从哪里看起呢,上节我们的示例中,其中修改数据时加写锁的代码细节:

public class ReentrantReadWriteLockTest {
    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    // 写锁
    private static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
    // 共享变量
    private static int value = 0;
    
    public static void addValue() {
        try {
            // 修改数据前,加写锁
            writeLock.lock();
            // 修改数据
            value++;
        } finally {
            // 写锁释放
            writeLock.unlock();
        }
    }
}

我们知道,写锁的加锁是通过调用WriteLock.lock方法,释放锁是调用WriteLock.unlock方法,那么接下来就分析加锁lock、释放锁unlock的底层源码。

2.1  写锁的加锁lock源码分析

作为加锁的入口lock方法源码如下:

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

调用的是Sync的acquire方法,说明只是对Sync同步器进行封装而已。
Sync继承自AQS,所以进入到AQS的acquire;之前讲解AQS以及其它互斥锁的时候,已经讲过了,AQS的acquire是一个模板方法,定义了几个模板流程,如下:

public final void acquire(int arg) {
    // 流程1,进来先调用子类的tryAcquire,这里才是实际上获取资源的实现方法
    // 如果返回false表示获取锁失败,true表示获取锁成功
    if (!tryAcquire(arg) &&
        // 流程2,获取资源失败,则调用addWaiter将当前线程加入AQS的等待队列
        // 流程3,调用acquireQueue方法在AQS等待队列中沉睡,或者再次尝试获取锁
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

上面的模板流程方法acquire定义了几个流程,分别为:
(1)流程1,进来先调用子类的tryAcquire,这里才是实际上获取资源的实现方法,true则说明获取锁成功,直接返回;false则获取锁失败,走流程2、流程3
(2)流程2,获取资源失败,则调用addWaiter将当前线程加入AQS的等待队列
(3)流程3,调用acquireQueue方法在AQS等待队列中沉睡,或者再次尝试获取锁
其中流程2、流程3由AQS来实现,都是一样的,唯一不同的是流程1的tryAcquire方法由子类实现,我们根据上述的源码流程得出以下流程图:

上面的流程图就是写锁WriteLock的lock源码的流程图,其中tryAcquire获取锁的具体实现在AQS的子类Sync中,我们继续往下看。

2.1.1  Sync的tryAcquire方法源码

protected final boolean tryAcquire(int acquires) {
    // 获取当前线程
    Thread current = Thread.currentThread();
    // 获取当前读写锁的状态state
    int c = getState();
    // 计算写锁的加锁次数,上一节讲过此方法,保留低16位的值便是写锁个数
    int w = exclusiveCount(c);
    // 如果c!=0,说明有别人加了写锁,或者加了读锁
    if (c != 0) {
        // 1.如果w==0写锁个数为零,说明上面加的锁是读锁,2.当前线程不是获取写锁的线程
        // 这里的意思就是,1.有人加了读锁 2.有人加了写锁,但是加锁的人不是自己,读写、写写互斥,那么自己加锁失败
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        // 走到这里说明w!=0,且current==getExclusiveOwnerThread()说明之前加写锁的人是自己
        // 这里只需要进行重复,增加加锁次数就可以了
        // 同时判断写锁次数最多可以加锁65535次,是否都用完了
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // 重新设置加锁的次数
        setState(c + acquires);
        // 加锁成功返回true
        return true;
    }
    // 走到这里,c == 0 说明读锁、写锁都没有人加
    // writeShouldBlock这里是有公平锁FairSync、非公平锁NonfairSync两种实现
    // 公平锁这里判断AQS等待队列是否有线程在等待,有则不去获取锁,自己也去等待
    // 非公平锁这里直接去尝试获取锁,不管AQS等待队列是否有线程在等待
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    // 走到这里,加锁成功,设置加锁线程为自己
    setExclusiveOwnerThread(current);
    // 返回加锁成功
    return true;
}

根据上面的Sync的tryAcquire源码,我们画个图来理解一下:

(1)首先进来先判断一下c != 0,如果c != 0 说明有人加了锁,可能是写锁、也可能是读锁
(2)如果c!=0 有人加了锁,计算下w = exclusiveCount(C) 写锁的加锁次数。
如果w == 0,说明写锁次数为零,那么之前加的锁必定是读锁,此时读写互斥,加写锁失败。
如果 w!=0,且current != getExclusiveOwnerThread()说明有人加了写锁,但是加锁的不是自己,由于写写互斥,此时自己加写锁也是失败的。
(3)如果上面(2)均不成立,则说明w!= 0 且 current == getExclusiveOwnerThread,说明之前自己已经加了写锁,此时直接进行锁重入即可,也就是将加锁的次数增加,同时判断是不是加锁次数到达了最大上限。
(4)如果c == 0 说明之前读锁、写锁都没有人加,此时根据writeShouldBlock判断是否允许进行加锁。
这里的writeShouldBlock 有公平模式、非公平模式两种实现。
公平模式就是判断AQS等待队列有人在等待,则不允许加锁,你需要去后面排队;非公平模式不管有没有人排队,允许你上来就抢。
加锁需要执行CAS操作争抢,如果成功则设置加锁线程是自己,设置加锁次数等,失败则直接返回false。

2.1.2  整体加锁过程

我们再从整体上画一个WriteLock.lock方法的整体流程图:

上面的WriteLock.lock的源码流程已经完全讲解完毕了,接下来我们就进入WriteLock.unlock释放锁的源码分析。

2.2  写锁的释放锁unlock源码分析

unlock源码如下:

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

直接是封装的Sync的release方法,而Sync继承自AQS,所以调用的是AQS的release模板流程方法,源码如下:

public final boolean release(int arg) {
    // 流程1,直接调用子类的tryRelease方法释放锁
    if (tryRelease(arg)) {
        Node h = head;
        // 流程2,返回true释放成功,唤醒AQS中后一个正在等待的线程
        if (h != null && h.waitStatus != 0)
            // 唤醒头节点后一个正在等待锁的线程
            unparkSuccessor(h);
        return true;
    }
    return false;
}

其中release方法也是AQS的模板流程方法,具体模板步骤如下:
(1)流程1,调用子类的tryRelease方法释放锁,返回true释放成功,false失败
(2)流程2,如果释放成功,则调用unparkSuccessor唤醒后继节点,也就是唤醒头节点的下一个节点(第二节点)

unparkSuccessor的具体源码我们在讲AQS的时候已经讲解过了,所以这里我们分析下Sync的tryRelease方法源码。

2.2.1  Sync的tryRelease方法源码

protected final boolean tryRelease(int releases) {
    // 这里校验一下,持有锁的是否是当前线程
    // 只允许持有锁的线程释放锁,否则是非法操作
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 这里释放的逻辑就是将加锁的次数减少而已
    int nextc = getState() - releases;
    // 计算释放后的写锁加锁次数,如果写锁的加锁次数为0了,那么说明完全释放了
    boolean free = exclusiveCount(nextc) == 0;
    // 如果写锁完全释放了,则设置加锁的线程为null,也就是没人加锁
    if (free)
        setExclusiveOwnerThread(null);
    // 重写设置锁变量
    setState(nextc);
    return free;
}

我们再画个图来理解一下:

这里的unlock源码的流程比较简单,释放锁的逻辑在Sync的tryRelease方法里面:
(1)第一步判断加锁线程是不是自己,如果不是自己加锁,则不允许自己释放,抛出异常。
(2)然后就是扣减写锁的次数,然后判断扣减之后写锁的次数是否为 0,为 0 说明完全释放了,则设置一下加锁线程为null,表示完全释放了。
(3)如果释放锁成功了,则调用AQS的unparkSuccessor唤醒后续在等待的节点。

3  小结

到这里ReentrantReadWriteLock中写锁WriteLock加锁、释放锁的底层原理和源码都的差不多,下节我们看读锁,有理解不对的地方欢迎指正哈。