实际场景解读AQS读写锁ReentrantReadWriteLock

本文详细介绍了AQS在ReentrantReadWriteLock中的应用,通过逐步分析代码,展示了读写锁的加锁、解锁过程,包括线程如何基于AQS的state完成加锁、写锁可重入判断、线程阻塞与唤醒等关键步骤,旨在帮助读者深入理解读写锁的工作原理。
摘要由CSDN通过智能技术生成

image.png

前面讲了什么是AQS.并且基于Reentrantlock已经看了一部分AQS方法. 建议看完下面这篇再来阅读该文.
AQS是什么?基于ReentrantLock解密!
但是其实真实项目中一般不使用ReetrantLock,一般都是synchronized. 因为在jdk1.6之后,synchronized底层实现里面,里面做了一些计数器的维护,加锁释放锁,CAS啊.都能实现ReentrantLock一样的效果.而且使用起来相对简单. 另外读写锁ReentrantReadWriteLock其实也挺常用的
下面就接着看对于ReentrantReadWriteLock底层是如何基于aqs和Lock API进行读写锁的实现的.

前言

  • 如果想要几分钟了解AQS是不现实的,你要这样想,人家大神花了几个月甚至几年写的代码怎么会那么容易几分钟让你就看懂呢?
  • 本文会根据一步一图的形式,将代码中的各种指针变化全部画出来.便于理解.因为不画图是根本无法了解流程是怎么走的.
  • 再讲一遍,该文每个场景都要跟着一步一图走,单看文章可能无法理解,需要结合源码.可以边看边打开idea跟着方法一块儿走.顺便看着我画的图在process on等画图工具里面跟着一起画.理解流程.这样才能更快的理解里面及其复杂的各种指针变化.
  • 本文阅读完要20几分钟,但是如果你跟着一起画图,一起看源码,直到看明白,其实是远远不止的.所以做好心理准备,开始吧.

LockAPI的读写锁简单介绍

首先明确一点,读锁是共享锁,而写锁是独占锁.

  • Lock API,读写锁,可以加读锁,也可以加写锁

  • 但是,读锁和写锁是互斥的,也就是说,你加了读锁之后,就不能加写锁;如果加了写锁,就不能加读锁

  • 如果有人在读数据,就不能有人写数据,读锁 -> 写锁 -> 互斥

  • 如果有人在写数据,别人不能写数据,写锁 -> 写锁 -> 互斥;

  • 如果有人在写数据,别人也不能读数据,写锁 -> 读锁 > 互斥

  • 但是如果有人加了读锁之后,别人可以同时加读锁

    • 如果你有一份数据,有人读,有人写,如果你全部都是用synchronized的话,会导致如果多个人读,也是要串行化,一个接一个的读

    • 我们希望的效果是多个人可以同时来读,如果使用读锁和写锁分开的方式,就可以让多个人来读数据,多个人可以同时加读锁

读写锁的好处

  • 读写锁维护了一对锁,一个读锁和一个写锁,通过分离读写锁,使得并发性比一般的排它锁有了很大提升。
  • synchronzied特性以及功能,读写锁都拥有
  • 但是因为大多数应用场景都是读多于写的,因此在这样的情况下,读写锁可以提高吞吐量。这是相对于synchronized好的地方.

ReentrantReadWriteLock的demo使用

线程1写锁如何基于AQS的state完成加锁的?

  • 我们走进writeLock的lock方法中.可以看到下面方法.首先调用tryAcquire方法.获取到了当前线程1.会判断是否有其他线程抢到过锁image.png

acquire方法如下

public final void acquire(int arg) {
   
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
   
    Thread current = Thread.currentThread();
    // 获取到一个state = 0
    int c = getState();
    // 二进制值里面的高低16位分别代表了读锁和写锁,AQS就一个,state
    // state二进制值的高16位代表了读锁,低16位代表了写锁
    // 可以认为下面的w就是从c(二进制值)通过位运算获取到了state的低16位,代表了写锁的状态
    int w = exclusiveCount(c);
    // 如果c != 0,说明有人加过锁,但是此时c = 0
    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;
    }
  • 上面的c也就是state第一次进去是为0的.证明没有其他线程抢到锁,所以会走第二个if语句.进行writerShouldBlock判断是否公平,
    • 如果是非公平策略.writerShouldBlock会返回false,此时一定会去执行compareAndSetState(c, c + acquires) 尝试加锁,state+1
    • 如果是公平策略.writerShouldBlock则会判断是否阻塞队列里面有元素在排队,没有返回false,执行cas,state+1,有就返回true 直接跳过if
  • 然后会设置当前加锁线程为线程1.

总结

当第一个线程过来尝试加锁的时候,会cas成功后对state加1,将当前加锁线程存进变量ExclusiveOwnerThread中

线程1state二进制高低16位判断写锁可重入

其实说白了就是将state的值在加1,比如下面线程1的state本来加过一次锁.再加一次就会变为2.
image.png

代码分析

下面看代码,其实还是tryAcquire方法

 protected final boolean tryAcquire(int acquires) {
   
     Thread current = Thread.currentThread();
     int c = getState(); //获取到state此时为1,
     int w = exclusiveCount(c);//w变量代表write写锁的第十六位二进制数,exclusiveCount获取的低十六位的值此时也是不为0的.
     if (c != 0) {
   
         //虽然c!=0,w!=0,但是因为当前加锁的线程还是线程1.所以if中的第二个条件不满足
         if (w == 0 || current != getExclusiveOwnerThread())
             return false;
         //这里会判断加写锁数量是否大于最大数量65535.如果超过就抛异常.  不满足条件.
         if (w + exclusiveCount(acquires) > MAX_COUNT)
             throw new Error("Maximum lock count exceeded");
         //这里会重新设置state的值为2.然后返回true
         setState(c + acquires);
         return true;
     }
     if (writerShouldBlock() ||
         !compareAndSetState(c, c + acquires))
         return false;
     setExclusiveOwnerThread(current);
     return true;
 }

总结

  • 如果是int类型的state不是0的话,那么他的二进制数值,32位,低16位一定不是0,如果低16位不是0的话,就代表他是加过写锁的

c != 0,w == 0,c肯定不是0,但是低16位是0,说明高16位不为0, 此时有线程加了读锁,没有线程加写锁,此时线程1要加写锁,而且线程1还不是之前加锁的那个线程
c != 0,w != 0,有线程加过锁,之前加的是写锁,但是当前线程不是之前加锁的线程,此时也不让线程1加写锁,同一个时间,只能有一个线程加写锁,如果线程1比如加了写锁,线程2也要加写锁,是互斥的
c != 0,w != 0,之前有人加过写锁,而且加写锁的还是线程1
如果加写锁的人是线程1,说明线程1就是在可重入的加写锁,将state += 1

线程2写锁加锁失败如何基于CAS队列阻塞等待?

当tryAcquire返回false的时候证明此时加锁的不是线程1,而是线程2了.此时就要进行阻塞线程2了.

 public final void acquire(int arg) {
   
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

addWaiter方法如下

  • 这里流程其实就跟ReentrantLock中分析的时候一样.就是各种指针变化.
     */
    private Node addWaiter(Node mode) {
   
        Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail;
        if (pred != null) {
   
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
   
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
  • 我们一步步分析,首先基于当前抢锁失败线程创建一个node.然后pred指针指向tail.此时tail为null,所以pred也为null

image.png

enq方法如下

  • 然后进入enq方法
 private Node enq(final Node node) {
   
        for (;;) {
   
            Node t = tail;
            if (t == null) {
    // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
   
                node.prev = t;
                if (compareAndSetTail(t, node)) {
   
                    t.next = node;
                    return t;
                }
            }
        }
    }
  • 首先t也指向tail.然后tail为null,那么t也为null,然后就进行初始化虚拟Node设置为一个Head节点,并且将tail指针指向nodeHead节点.

image.png

  • 然后二次循环,此时t重新指向tail,然后判断不为null,就进入else,将线程2node的prev指向NodeHead,然后tail节点设置成线程2node,nodeHead的next指针指向线程2node,形成回路,最后返回nodeHead如下图.

image.png

acquireQueued方法

  • enq执行完之后addWaiter方法返回线程2node

acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 也就是acquireQueued(线程2node)

final boolean acquireQueued(final Node node, int arg) {
   
        boolean failed = true;
        try {
   
            boolean interrupted = false;
            for (;;) {
   
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
   
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
   
            //这里tryAcquire不报错就不会执行.暂时不管,想了解cancelAcquire的话建议看ReentrantLock,那个文档有写
            if (failed)
                cancelAcquire(node);
        }
    }
  • 首先获取node前驱节点,这里线程2node的前驱节点就是nodeHead,所以第一个if中虽然p==head,但是我们这里假设再次执行tryAcquire还是返回false,那么就会继续走shouldParkAfterFailedAcquire(nodeHead,线程2Node)方法了.

shouldParkAfterFailedAcquire方法

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
   
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值