浅谈AQS的基本原理

AQS是什么?

AQS,全称AbstractQueuedSynchronizer,抽象队列同步器,是用来构建锁或者其他同步组建的基础框架;它使用了一个int成员变量state来表示同步的状态,并内置了一个队列来完成需要获取资源的线程的排队工作;锁是面向锁的使用者的,而AQS则是面向锁的编写者的

AQS的工作原理是什么?

三个组件:state,队列,exclusiveOwnerThread

state:实现锁信息的同步

CLH队列:对获取锁失败的线程进行管理

exclusiveOwnerThread:用于表示当前是哪个线程正在持有锁

AQS依赖底层的同步队列,当一个线程获取锁失败之后,那么就会将其封装为一个node节点然后到队列的末尾,并阻塞这个线程,当持有锁的线程释放之后,会将队头的节点中的线程唤醒,使其再次尝试获取锁

AQS的node节点的状态值有哪些?

cancelled:节点引用的线程由于等待超时或者打断被取消之后,节点会变成cancelled状态

signal:后续节点需要被唤醒时,当前节点就会变成signal状态,

condition:表示当前节点进入了condition队列的状态

propagate:释放共享锁的时候会头节点使用

AQS的阻塞+自旋是在哪个方法里面?

在acquireQueued里面

java 复制代码
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 {
        if (failed)
            cancelAcquire(node);
    }
}

AQS中acquire的工作流程是怎么样的?

java 复制代码
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  1. 使用tryAcquire尝试获取锁,获取锁成功,直接返回,如果失败,进入第2步

  2. 进入addWaiter,将当前线程封装成一个node,然后放入CLH队列的尾部

  3. 进入acquireQueued

    • 当前节点的前驱节点是CLH队列的头节点,那么通过tryAcquire来尝试获取锁,如果获取成功,那么把当前节点设置为头节点

    • 当前节点的前驱节点不是CLH队列的头节点,或者tryAcquire获取锁失败,那么会进入shouldParkAfterFailedAcquire并判断它的前驱节点的waitStatus状态:

      • 如果是signal,返回true
      • 如果是cancel,那么会找到从这个节点开始往前数,第一个不是cancel状态的节点,并把这个节点当成自己的头节点,删除掉中间的所有cancel节点,返回false
      • 如果是其他情况,那么将前驱节点的waitStatus状态修改为signal,返回false

      如果shouldParkAfterFailedAcquire返回true,那么阻塞,否则进入下一次循环

  4. 如果阻塞时被中断,那么进入selfInterrupt,打断当前线程

AQS为什么要设计为双向链表?

因为有的操作需要使用到前驱节点

当判断这个线程是否需要阻塞的时候,需要判断前一个前驱节点的waitStatus状态是否为signal

另外如果前驱节点是一个cancelled状态的节点,那么往前找到第一个不是cancelled状态的节点位置

基于AQS实现的ReentrantLock在非公平的情况下的lock和unlock过程是怎么样的?

lock

java 复制代码
public void lock() {
    sync.lock();
}
java 复制代码
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     */
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}
java 复制代码
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
java 复制代码
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

在调用lock之后,会通过cas判断state是否等于0并尝试把它设置为1(尝试加锁),如果cas成功,那么会把锁的独占线程设置为当前线程,这是非公平锁的原因;

如果加锁失败,会进入acquire,然后进入tryAcquire方法,然后会再一次判断state,如果state等于0,那么会再尝试加一次锁,如果state不等于0,那么会判断要当前线程是否是持有锁的线程,如果是,那么会将state加1,这也是可重入的原因;

如果tryAcquire没有获取到锁,那么会将这个线程封装成一个node,会通过cas将结点插入到aqs链表的尾部;

然后在acquireQueued中,通过cas+自旋的方式尝试获取锁,但是只有当前结点时是头结点的时候才能尝试获取锁,这中间如果遇到了中断,那么是不会抛出异常的(使用lockInterruptibly则会抛异常),会继续自旋并阻塞,直到获取到锁为止;

unlock

java 复制代码
public void unlock() {
    sync.release(1);
}
java 复制代码
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
java 复制代码
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}
java 复制代码
private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

释放锁相对比较简单,会先判断释放锁的线程是不是持有锁的线程,如果是,那么就会释放锁,由于是可重入锁,所以需要等state变成0之后才能完全释放锁,把持有锁的线程置为null并唤醒后面的线程;

AQS的模板方法模式体现在哪里?

acquire

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

acquire方法中,先定义了获取锁的流程是tryAcquire,获取失败之后通过addWaiter把线程包装成一个结点放入aqs队列,然后再通过acquireQueued自旋获取锁;这一套流程已经定义好,具体的实现可以由子类去重写tryAcquire方法来决定

acquireInterruptibly

java 复制代码
public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}

acquireShared

java 复制代码
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

acquireSharedInterruptibly

java 复制代码
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

release

java 复制代码
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

releaseShared

java 复制代码
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

AQS的哪些方法需要被子类重写?

tryAcquire、tryRelease、tryAcquireShared、tryReleaseShared、isHeldExclusively

AQS没有抽象方法,为什么要使用abstract修饰?

AQS设置为抽象类的原因是它必须要实现其中的某一些方法才能使用,比如tryAcquire,tryRelease等核心方法,作者可能并不希望使用者直接初始化AQS并使用,所以使用abstract修饰

AQS中那些未实现的方法为什么不声明为抽象方法?

因为AQS是大框架,很多同步组件都是基于AQS开发,其中有一些组件可能是需要实现其中的两个或者三个方法即可,不需要重写全部方法,如果把那些未实现的方法声明为抽象方法,那么每个方法都要重写,可能对锁的编写者来说不那么方便

Condition的工作原理?

Condition是aqs中的一个内部类,可以通过await和signal实现更加精细的线程同步机制,类似与object的wait和notify

Condition内部有一个双向链表,当线程调用了Condition的await之后,这个node会被放到Condition队列的尾部中,释放锁,并失去获取锁的资格,不断自旋阻塞等待重回CLH队列;

当其他线程调用这个Condition的signal之后,这个node会从Condition队列的头部中移除,并加入到CLH队列的尾部中等待获取锁,会带上锁的重入次数;

Condition的await、signal和Object的wait、notify有什么区别?

Condition的await和signal是配合lock使用的,Object的wait和notify是配合sync使用的

Condition可以响应中断,Object不行

同一个lock的情况下,Condition可以有多条Condition队列,Object只会有一个

相关推荐
程序员南飞1 小时前
ps aux | grep smart_webrtc这条指令代表什么意思
java·linux·ubuntu·webrtc
弥琉撒到我1 小时前
微服务swagger解析部署使用全流程
java·微服务·架构·swagger
一颗花生米。2 小时前
深入理解JavaScript 的原型继承
java·开发语言·javascript·原型模式
问道飞鱼2 小时前
Java基础-单例模式的实现
java·开发语言·单例模式
ok!ko6 小时前
设计模式之原型模式(通俗易懂--代码辅助理解【Java版】)
java·设计模式·原型模式
2401_857622666 小时前
SpringBoot框架下校园资料库的构建与优化
spring boot·后端·php
2402_857589366 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
吾爱星辰7 小时前
Kotlin 处理字符串和正则表达式(二十一)
java·开发语言·jvm·正则表达式·kotlin
哎呦没8 小时前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
小飞猪Jay8 小时前
C++面试速通宝典——13
jvm·c++·面试