AbstractQueuedSynchronizer理解以及ReentrantLock和ReentrantReadWriteLock锁对AQS的实现

AQS整体架构与设计原则分析

AbstractQueuedSynchronizer(抽象的队列同步器),它是一个抽象类,所以一般使用它的时候都是写一个类来继承。它大部分代码都是基于Java来实现的。

本质上,AQS的与Synchronized在底层上的实现相似度能达到80%以上。synchronized 是独占锁,当一个线程获取到这把锁以后,其他线程无法获取到,只有等这个线程退出,释放掉锁之后,才能获取。

AQS的某些子类的实现,实现了读写锁的分离。对于一些数据的读取,我们对它上锁是没有必要的,这种锁我们称它为读锁\ShareLock(共享锁);而对于数据的写,它显然是一个排它锁,比如一个线程在操作一个数据的时候,另外一个线程无论是读取还是写入都不能操作了。

1.宏观角度了解AQS

(1)AQS除了自己定义了若干个方法以外,自己内部还定义了两个类: Node、ConditionObject(实现了 Condition接口)

(2)对于Node来说,它本身维护了一个 FIFO(first-in-first-out)这样的一个等待队列,是一个双向的对列。Node还定义了一个 thread 对象,AQS会把受阻塞的线程,包装成一个Node对象,使这些线程形成一个FIFO这样的一个队列,队列当中就能进行一个顺序的处理。
(附上一张截图)
在这里插入图片描述
AQS和condition可以是一个对多个的关系,一个condition对应Queue队列。这就涉及到了处于condition条件变量下面的那些线程转换到 FIFO阻塞队列的过程?如果线程调用了 signal或者 signalAll方法被唤醒之后,就立刻拿到资源,进入 FIFO阻塞队列中。

(3)AQS里面有一个很重要的成员变量,它决定当前线程受允许拿到AQS执行的资源。
在这里插入图片描述
state这个成员变量很重要,不管是基于AQS生成的共享锁还是排它锁,都会有state这个变量,只是区别在于不同锁,state的含义是不一样的。

  • 如果是ReentrantLock这样的可重入锁,典型的排它锁,如果一个线程调用 lock方法,其他线程就不能执行。 如果是相同的线程,调用可重入锁的 lock方法,这个state 的值就会加一,直到它调用 unlock方法减为0,其他线程才能拿到资源,执行。
  • 而读写锁也是基于这个 state成员变量来判断的。int 类型的 state是4个字节,占用36位。ReentrantReadWriteLock 就把32位分为高16位和低16位。高位是读锁,低位是写锁
    (lower one representing the exclusive (writer) lock hold count,and the upper the shared (reader) hold count.)
    在这里插入图片描述
    所以我们必须要通过AQS具体的子类来去了解 state 具体代表着什么含义…

对于我们在并发包中常用的两个组件就是:ReentrantLock、ReentrantReadWriteLock

---------------------------------------------------------我是分割线-------------------------------------------
---------------------------------------------------------我是分割线-------------------------------------------

可重入锁ReentrantLock对于AQS的实现源码分析

代码示例:

public class MyTest1 {
    
    

    private Lock lock = new ReentrantLock();
    public void method() {
    
    
        try {
    
    
            lock.lock();
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("method");

        } finally {
    
    
            lock.unlock();
        }
    }
    public static void main(String[] args) {
    
    
        MyTest1 test1 = new MyTest1();

        IntStream.range(0, 10).forEach(i -> {
    
    
            new Thread(() -> {
    
    
                test1.method();
            }).start();
        });
    }
}

AQS里面有几个很重要的方法

排它模式:

  • boolean tryAcquire(int arg): Attempts to acquire in exclusive mode. 翻译过来就是在排它模式下面获取锁。源码直接抛异常,具体实现是由子类去实现
  • boolean tryRelease(int arg) :Attempts to set the state to reflect a release in exclusive mode. 翻译过来就是尝试去设置 state 这个成员变量的值,从而达到在排它模式下面释放锁。

共享模式:

  • int tryAcquireShared(int arg): Attempts to acquire in shared mode. 翻译过来就是在共享模式下面获取锁。源码直接抛异常,具体实现是由子类去实现
  • int tryAcquireShared(int arg):共享锁的释放

额外还有一个方法就是:boolean isHeldExclusively():对于锁判断的方法。 如果这个锁是排它锁,就返回 true,共享锁就返回 false.

当我们调用unlock方法时:最后调用在 ReentrantLock 的 tryRelease方法里面,就会释放锁。
在这里插入图片描述
(1).首先会将 state 这个局部变量的值减 1 ,然后判断当前的线程是否是加锁的线程(不能是A线程加锁,B线程去释放A线程加的锁)。
(2).判断state 的值是否为 0 ,如果为 0 ,就代表没有线程持有锁,其他线程就可以来持有锁
(3).最后设回 state 的值

对于 ReentrantLock 的总结

对于ReentrantLock 来说,其执行逻辑如下所示:

1.尝试获取对象的锁,如果获取不到(意味着已经有其他线程持有了锁,并且尚未释放),那么它就会进入到 AQS 的阻塞队列当中

2.如果获取到了,那么根据锁是公平锁还是非公平锁来进行不同的处理

  • 如果是公平锁,那么线程会直接放置到 AQS 的阻塞队列的末尾
  • 如果是非公平锁,那么线程会首先尝试进行 CAS 计算,如果成功,则直接获取到锁。如果失败,则与公平锁的处理方式一样,被放到阻塞队列末尾

3.当锁被释放时,(调用了 unlock方法),底层会调用 release 方法会对 state 成员变量值进行减一操作。如果减一后, state值不为0,那么 release 操作就会执行完毕。如果减一操作后, state 值为0,则调用 LockSupport 方法唤醒该线程后的等待队列中的第一个。后继线程(pthread mutex unlock),将其唤醒,使之能够获取到对象的锁(release时,对于公平锁与非公平锁的处理逻辑是一致的)。之所以调用 release 方法后 state 值可能不为 0 ,原因在于 ReentrantLock 是可重入锁,表示线程可以多次调用 lock 方法
在这里插入图片描述
在这里插入图片描述

上述两张截图就能说明:在发现锁被上一个线程完全释放之后,就会去等待队列 Node 里面拿取第一个线程,然在后面的 unparkSuccessor 方法里面唤醒(Wakes up node’s successor, if one exists.)

对于 ReentrantLock 来说,所谓的上锁,本质上就是对 AQS 中的 state 成员变量的操作;对该成员变量+1 ,表示上锁;对该成员变量-1,表示释放锁


-----------------------------------------------------------我是分隔线------------------------------------------
-----------------------------------------------------------我是分隔线------------------------------------------

可重入读写锁ReentrantReadWriteLock对于AQS的实现源码分析

代码示例:

public class MyTest2 {
    
    

    private ReadWriteLock lock = new ReentrantReadWriteLock();

    public void method() {
    
    
        try {
    
    
            // lock.readLock().lock();
            lock.writeLock().lock();
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
    
    
                ex.printStackTrace();
            }

            System.out.println("method");

        } finally {
    
    
            //lock.readLock().unlock();
            lock.writeLock().unlock();
        }
    }

    public static void main(String[] args) {
    
    
        MyTest2 myTest2 = new MyTest2();
        IntStream.range(0, 10).forEach(i -> new Thread(myTest2::method).start());
    }
}

运行结果分析:

  • 获取 读锁 的结果是10个线程的执行结果在1秒后一起被打印出来。
  • 获取 写锁 的结果是每间隔1秒才会有一个线程的打印结果被执行出来,然后10秒过后10个线程都执行完毕

猜你喜欢

转载自blog.csdn.net/weixin_43582499/article/details/112986526