都2021年了,还没掌握AQS,只因你不懂这个方法!!!

Posted CRUD速写大师

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了都2021年了,还没掌握AQS,只因你不懂这个方法!!!相关的知识,希望对你有一定的参考价值。



前言

相信各位小伙伴对于AQS这个词已经见怪不怪了,但是面对层层的源码,掌握的小伙伴却少之又少,即使看视频看着看着就迷糊了,那么今天由我皮皮虾来带各位解读AQS,并附上流程总结,相信看了的小伙伴们能有所收获!!


ReentrantLock底层实现原理为:AQS + CAS。

如果有对CAS还不熟悉的小伙伴可以看看我的这篇文章,很受好评 CAS原理刨析


AQS核心思想:如果请求的资源空闲,那么就将当前的请求设置为资源的持有线程,并将共享资源设置为锁定状态,如果请求的共享资源被占用,那么就需要一套线程等待阻塞以及被唤醒的机制,这个机制就是通过CLH队列来实现的,也就是将暂时获取不到锁的线程加入到队列中。


CLH:Craig、Landin and Hagersten 队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。

图示如下:

在这里插入图片描述



lock()

通过 compareAndSetState 尝试获取到锁

  • 如果获取到锁,那么将该锁的持有线程设置为当前线程。且当前同步状态为1,即state = 1
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}

通过反射获取到 state字段赋值给 stateOffset

在这里插入图片描述

  • 如果没有获取到锁,则进去acquire(1)方法
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

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


tryAcquire()方法

如果该方法返回了True,则说明当前线程获取锁成功,就不用往后执行了;如果获取失败,就需要加入到等待队列中。

所以上面对tryAcquire()返回结果取反,如果成功了就不需要往下执行了

在这里插入图片描述

流程

  • 获取当前线程和当前的同步状态
  • 如果同步状态为0,那么再次进行CAS,将state修改为1,且将当前线程设置为持有锁的线程
  • 如果不是,就看当前线程是否是持有锁的线程
  • 如果还不是,那么返回 false
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}


final boolean nonfairTryAcquire(int acquires) {
    //获取到当前线程
    final Thread current = Thread.currentThread();
    
    //获取到当前 state 的值,也就是 1
    int c = getState();
    if (c == 0) {
        //CAS
        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;
}

protected final int getState() {
    return state;
}

为什么要这么设计?

假设:如果A线程已经抢到了锁,那么此时B线程走到了 final Thread current = Thread.currentThread(); 的时候,A线程释放了锁,那么此时 B 线程get到的state的值就是0,那么可以直接去进行CAS获取锁。

还有,针对第二个if else if (current == getExclusiveOwnerThread())

它的意思就是如果当前线程是持有锁的线程,那么就会继续往下执行,从而:

//因为 acquires 为 1,此时 c 为1 ,那么新的状态就是 2
int nextc = c + acquires;
if (nextc < 0) // overflow
    throw new Error("Maximum lock count exceeded");
//设置当前状态
setState(nextc);
return true;

这其实就是一个可重入锁 的原理,面试中也可以向面试官答出来,绝对是加分项!!!



addWaiter(Node.EXCLUSIVE) 方法

线程两种锁的模式:

模式含义
SHARED表示线程以共享的模式等待锁
EXCLUSIVE表示线程正在以独占的方式等待锁

在这里插入图片描述

当前线程封装为Node节点加入AQS双向链表队列。

先将当前线程封装为一个Node对象

  • 首先获取到尾节点判断队列是否为空
  • 不为空时则将封装好的 Node 利用 CAS 写入队尾,
  • 尾节点为空,说明队列还未初始化,则调用enq()方法,来初始化head节点并入队新节点,而且头节点是自己 new的一个空节点
private Node addWaiter(Node mode) {
    //封装为Node节点
    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()方法,来初始化head节点并入队新节点
    enq(node);
    return node;
}


private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { 
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}


acquireQueued

  • 如果当前节点的前驱节点是头节点,并且能够获得同步状态的话,当前线程能够获得锁该方法执行结束退出
  • 获取锁失败的话,先将节点状态设置成SIGNAL,然后调用LookSupport.park方法使得当前线程阻塞
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            //拿到当前节点的前一个节点
            final Node p = node.predecessor();
            //如果该节点是头节点,则调用tryAcquire方法
            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);
    }
}

final Node predecessor() throws NullPointerException {
    Node p = prev;
    if (p == null)
        throw new NullPointerException();
    else
        return p;
}

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    
    //获取前驱节点的状态
    int ws = pred.waitStatus;
    
    //如果是SIGNAL状态,即等待被占用的资源释放,直接返回 true
    if (ws == Node.SIGNAL)
        return true;
    //ws > 0 说明是 CANCELLED 状态
    if (ws > 0) {
        //循环判断前驱节点的前驱节点是否也为CANCELLED
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        //将当前节点的前驱节点设置为 SIGNAL 状态,用于后续的唤醒操作
        //程序第一次执行到这返回 false ,还会进行外层第二次循环,最终
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}


private final boolean parkAndCheckInterrupt() {
    //当前线程被阻塞,程序不会继续往下走
    LockSupport.park(this);
    return Thread.interrupted();
}


unlock()

  • 调用tryRelease()方法,先去获取同步状态,将当前状态减一,如果为0,则将锁的持有线程设置为null,再去更新同步状态为0

  • 如果head节点不为null,且节点状态不为0,那么就会去unparkSuccessor()方法,

    • 这个方法,先会去获取头节点的后继节点,如果这个节点不为null的话,那么就调用LockSupport.unpark()方法去唤醒该线程。
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}


protected final boolean tryRelease(int releases) {
    //讲当前state值减一
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    
    //如果同步状态值为0
    if (c == 0) {
        free = true;
        //设置当前锁持有线程为null
        setExclusiveOwnerThread(null);
    }
    //设置新的同步状态为 0
    setState(c);
    return free;
}
private void unparkSuccessor(Node node) {

    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);


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


总结

lock()

  • 调用ReentrantLock的lock()方法,其实是调用的的sync.lock()方法,这个Sync是ReentrantLock的一个抽象静态内部类继承了AQS(AbstractQueuedSynchronizer),而ReentrantLock默认是非公平锁,所以sync.lock()方法又是调用的 NonfairSync 类的 lock()方法,因为这个NonfairSync继承了Sync
  • 首先,当前线程会通过CAS去抢占锁
    • 如果抢占成功的话,那么就将当前锁的持有线程设置为该线程,并且将同步状态设置1
    • 如果没有成功的话,就会去调用 acquire(1) 方法
  • 那么 acquire(1) 中主要涉及到三个方法tryAcquire()、addWaiter()、acquireQueued()
    • tryAcquire()
      • 获取当前线程和当前的同步状态
      • 如果同步状态为0,那么再次进行CAS,将state修改为1,且将当前线程设置为持有锁的线程
      • 如果不是,就看当前线程是否是持有锁的线程
      • 如果还不是,那么返回 false
    • addWaiter()
      • 当前线程封装为Node节点加入AQS双向链表队列。
    • acquireQueued()
      • 如果当前节点的前驱节点是头节点,并且能够获得同步状态的话,当前线程能够获得锁该方法执行结束退出
      • 获取锁失败的话,先将节点状态设置成SIGNAL,然后调用LookSupport.park方法使得当前线程阻塞

unlock()

  • 调用tryRelease()方法,先去获取同步状态,将当前状态减一,如果为0,则将锁的持有线程设置为null,再去更新同步状态为0
  • 如果head节点不为null,且节点状态不为0,那么就会去unparkSuccessor()方法,
    • 这个方法,先会去获取头节点的后继节点,如果这个节点不为null的话,那么就调用LockSupport.unpark()方法去唤醒该线程。

以上是对ReentrantLock的非公平锁的解释,而ReentrantLock的公平锁相比于非公平主要是多了一个 hasQueuedPredecessors() 方法

hasQueuedPredecessors(),判断当前节点在等待队列中是否有前驱节点,如果有,则说明有线程比当前线程更早的请求资源,根据公平性,当前线程请求资源失败;如果当前节点没有前驱节点,才有做后面的逻辑判断的必要性。

在这里插入图片描述



尾言

我是 Code皮皮虾,一个热爱分享知识的 皮皮虾爱好者,未来的日子里会不断更新出对大家有益的博文,期待大家的关注!!!

创作不易,如果这篇博文对各位有帮助,希望各位小伙伴可以点赞和关注我哦,感谢支持,我们下次再见~~~

分享大纲

大厂面试题专栏


Java从入门到入坟学习路线目录索引


开源爬虫实例教程目录索引

更多精彩内容分享,请点击 Hello World (●’◡’●)


在这里插入图片描述

以上是关于都2021年了,还没掌握AQS,只因你不懂这个方法!!!的主要内容,如果未能解决你的问题,请参考以下文章

多线程和多进程不可能是鸡肋!只因你还不会!最全的进阶资料!

宝付提醒:被自动扣款只因你忽视了它

因为太穷,不敢做梦,只因你的操作系统还处于1.0!

一个小镇出身的程序员为何拒绝加入大厂?只因现在已经 2021年了...

因你不同,2021 阿里云开发者大会重磅开启 @ 所有开发者!

都8012年了,前端开发还值得“入坑”吗?