aqs原理_aqs源码解析(面试必问)_第一篇


前言

aqs的全称是AbstractQueuedSynchronizer,是juc的基石,而juc又是高并发代码中不可或缺的一部分。下面我们就从源码的角度去剖析aqs是原理。本人水平有限,如有误导,欢迎斧正,一起学习,共同进步!


提示:以下是本篇文章正文内容,下面案例可供参考

一、aqs源码

1、核心概念介绍

1.1、源码注释翻译

在这里插入图片描述
(举例第一段)
译文如下:
提供一个框架来实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关的同步器(信号量、事件等)。对于大多数依赖单个原子f@code int)值来表示状态的同步器来说,这个类是一个有用的基础。子类必须定义受保护的方法,这些方法可以更改此状态,并根据获取或释放此对象定义该状态的含义。考虑到这些,这个类中的其他方法执行所有的排队和阻塞机制。子类可以维护其他状态字段,但只有使用方法f@link #getState)、[@link #setState)和f@link #compareAndSetState)操作的原子更新的f@code int值才会被跟踪。

1.2、核心概念梳理:

aqs的注释写的挺全的,上面就是举个例子,感兴趣的同学可以自己点开源码翻译一下。下面是我的总结。
1、是一个底层框架,内部是基于队列的,有一个原子操作的int类型的字段state,来表示不同的含义。(操作这个state有getState、setState、compareAndSetState)就是说,aqs只是提供了一个int类型的state,具体的state代表的含义aqs不管,看具体的实现类。比如说,state在ReentrantLock中代表中state代表有锁、无锁;在countDownLatch中表示计数。
2、实现aqs的类,应该用内部类实现。比如说ReentrantLock就有一个内部类AbstractQueuedSynchronizer来实现加锁解锁的功能
3、有两种模式,exclusive(独占)、shared(共享)。实现类可以只实现其中一种模式,也可以同时实现两种模式(ReadWriteLock),aqs不关心具体实现哪种模式。
独占就是说,如果线程1已经占了资源了,线程2再来获取资源时就不会成功.
共享就是说,可以多个线程同时共享这个资源。
4、aqs有一个conditionObject,可以用来支持独占模式下
5、aqs为内部队列提供了检查、检测和监视方法
6、要使用这个类作为同步器的基础的话,需要重新定义一下方法:

  • tryAcquire、 (独占模式)
  • tryRelease、 (独占模式)
  • tryAcquireShared、 (共享模式)
  • tryReleaseShared、 (共享模式)
  • isHeldExclusively

7、推荐使用aos(AbstractOwnableSynchronizer),aos可以帮助你看是谁持有锁的线程是哪个线程。
8、即使aqs是基于FIFO的队列来实现的,但是也不会直接使用FIFO的模式,独占模式下的核心形式是:

Acquire:
      while (!tryAcquire(arg)) {
         <em>enqueue thread if it is not already queued</em>;
         <em>possibly block current thread</em>;
      }
      
Release:
      if (tryRelease(arg))
         <em>unblock the first queued thread</em>;

9、一个新的线程可能会比在队列中排队的线程更先获得线程,aqs将它称之为 “barge”。之所以会出现这个barge的原理是,先tryAcquire,才会入队,万一队列中的线程要获取资源的时候,突然来了新线程,就会出现多个线程同时竞争资源,队列中的线程有可能没抢的过新来的线程。 尽管如此,也可以通过某种方式来实现公平(公平锁非公平锁)。
10、非公平情况下,aqs的吞吐量和可伸缩性更大一些;非公平情况下,效率更低一些。(这很好理解:因为公平锁下,需要额外的维护一个队列来记录线程的顺序,所以肯定效率更第一些)

1.3、aqs源码给出的示例:

在这里插入图片描述

二、aqs原理

1、关键属性介绍

1.1、node

下面是源码中去掉注释的内容。

static final class Node {
        
        static final Node SHARED = new Node();
        
        static final Node EXCLUSIVE = null;

        static final int CANCELLED =  1;
     
        static final int SIGNAL    = -1;
       
        static final int CONDITION = -2;
        
        static final int PROPAGATE = -3;
  
        volatile int waitStatus;
     
        volatile Node prev;

        volatile Node next;
        
        volatile Thread thread;
     
        Node nextWaiter;
        
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

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

        Node() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

几个比较重要的参数:

SHARED:表示是个共享类型的节点。
EXCLUSIVE:表示是个独占模式的。

waitStatus:默认是0。取值有:

  • CANCELLED:中途取消的时候,状态码是1
  • SIGNAL:当前节点的下一个节点,状态码是-1
  • CONDITION:当前节点是一个条件队列里面的节点,状态码是-2
  • PROPAGATE:主动取消的时候才会设置这个类型,通常不会出现,状态码是-3
    默认情况下,取值是0。如果是条件队列里面的节点,他的值就是-2。如果要修改的话,需要使用cas操作。

prev:前驱节点。
next:后继节点。
thread:当前节点属于哪个线程。
nextWaiter:如果你的条件队列的话,它的nextWaiter就是在它后面排队的节点,如果不是条件队列的话,它会取固定的值“SHARED”。

1.2、同步队列

其实就是个队列,只不过给的概念是同步队列。一个FIFO的双向队列

1.3、条件队列

同“同步队列”

1.4、同步状态

其实就是state,在aqs中,1代表有锁,0代表无锁。在各个子类的具体实现中,state也代表不同的含义。比如说reentrantLock中,因为是可冲入的,所以state的值可能会比较大。

2、aqs执行原理

2.1、acquire(加锁)原理

在这里插入图片描述
aqs底层有一个node节点一样的东西,他存着上一个节点、下一个节点。aqs刚新建的时候,state是0,占有这个锁的线程是null。假设现在有1,2,3三个线程来竞争这个锁,假设线程1获取了这把锁,那么这个state就会变成1(从0新增到了1),占有这个锁的线程会变成1(1,2,3三个线程的1线程)。因为2,3没竞争到锁,就是我也竞争了,但是我没竞争到,那么他就会进排队队列中。2,3在竞争锁的时候发现state不是0了,说明已经有人占了这把锁了。如果2线程先发现的state不是0,那么2就会初始化一个队列(如果是3线程先发现的state不是0,那么就会是3线程初始化这个队列),这个队列是用来存排队的线程的。当然,如果1线程持久锁非常短,就是线程2(线程2代指其他线程)看的时候,state=0,没有人有这个锁,那么就不会初始化队列。线程2初始化队列以后,就会把自己挂到这个等待队列中去(挂进去是说,把这个节点的前一个节点指向头节点的下一个节点)。线程3进来排队的时候,也自动会把线程3节点的前一个节点的位置执行线程2的尾节点。上面这都是入队列操作。一个线程进入等待队列中有两部:1、入队列 2、申请入队(只有等待队列的第一个节点会去看)。 第一步就是把结构先创建出来,也就是上面的步骤,第二步是说,我创建好结构以后,我不甘心,我还想再试一次,看看state是不是0了,如果是0,则证明之前占有锁的线程已经释放了,我就可以占了。如果不是0,我就老老实实的进等待队列去等待(阻塞)。就是说,我准备好队列以后,我还想再看一眼,锁是不是被释放了,如果已经被释放了,我就不用阻塞了, 如果没有被释放,那么我就去阻塞去(必须是阻塞队列的第一个节点才有这个申请入队的操作,如果它不是队列的第一个节点,都不会有这部。你看个鬼啊,你前面都有人排着呢,肯定轮不到你,你就直接老老实实的挂起就行)。

2.1、release(放锁)原理

在这里插入图片描述

当reiease锁的时候,首先是判断当前线程和持有锁的线程是不是同一个线程。如果是的话,就将锁的状态设为无锁(state从1变成0),然后持有锁的线程从线程1变成null,在下一步是,唤醒头结点的下一个节点。唤醒(进等待队列的时候,这个线程就阻塞了,需要被唤醒)起来头结点的下一个节点里面的等待线程,就一定会抢到这个锁吗,也不一定,万一你刚唤醒,此时来了一个新的线程5,这个5线程还没进等待队列呢,那此时就是线程5和线程2(刚被唤醒)这俩线程去竞争锁资源(这是非公平锁的时候的场景)。如果线程2竞争成功了,就会改锁的状态(从0变成1),改持有锁的线程(从null变成线程2),并且线程2现在变成等待队列的头了(下次就从队列2的下一个节点开始问了),原先的线程2前面的头节点就没人用了,就等待gc的回收就行。如果没占用锁,那就会继续阻塞,等待下一次竞争的机会。

2.3、共享模式下的acquire

共享模式下的acquire,是仅把state的状态改了(从0改成1),不会把占了这个锁的线程改了(独占模式下是从null变成为占了这个锁的线程;共享模式下不操作这个占了锁的线程,还是null)。比如线程1,2,3这三线程来抢夺这个锁的资源。假设线程1抢到了,那么线程2、3要不要进等待队列呢?这就看aqs的各个子类具体的实现了。不过aqs是支持共享锁可以被多个线程同时拿到的。比如countDownLatch实现aqs的时候,就不允许多个线程同时持有锁,那么线程2、3就会进等待队列去等待。就是同样的步骤,先发现的线程去创建队列,然后进队列,只不过,node节点中的nextWaiter的属性是shared(如果不是共享锁,这个字段没用,独占模式下,这个字段是null),如果排队的话,就会被挂起。

2.4、共享模式下的release

共享模式下的release第一步肯定是将锁的状态改变(state从1变成0),第二步是唤醒等待队列的头结点的下一个节点(假设是线程2),第三部是判断已经唤醒的节点的下一个节点(线程2的下一个节点,假设是线程3),判断这个节点的waitState是不是shared,如果是的话,则继续唤起该节点(线程3),然后线程3的下一个节点在继续唤醒,看看是不是shared类型,如果是的话,都去唤醒,然后公共的去操作这个锁资源。(相当于比独占模式下多做了一步,独占模式下,我被唤醒了,我就直接去搞资源;共享模式下,我呗唤醒了,我去拿到锁的时候,我还去看看我后面的节点是不是共享类型,如果是的话,我把我下面的节点也唤醒,反正共享模式下的锁是可以同时被多个线程共享的)

在这里插入图片描述

aqs中还有一个重要的东西叫做条件队列。条件队列的原理是依赖于node节点的 Node nextWaiter 这个属性来实现的。
有俩方法,一个是await、一个是signal。

2.5(条件队列的)await的原理:

前面和acquire一样。线程1,线程2来竞争锁资源的时候,假设线程1抢到了锁资源,那么线程1就把state的状态改了(从0变成1),然后占有锁的线程名改了(改成线程1),如果此时线程2来了,如果此时条件队列是空,那么就会初始化一个条件队列,然后把线程2的节点放到条件队列中去。如果条件队列此时已经存在,那么就会把线程2的节点放到这个条件队列中去。然后会释放当前线程占有的锁,执行aqs的release操作。然后把当前线程挂起来。总结就是:
1、创建条件队列,必要的时候创建条件队列,或者吧自己放入条件队列中去
2、释放当前线程占有的锁,执行aqs的release操作
3、挂起当前占有锁的线程

2.6、(条件队列的)signal的原理:

把条件队列中的firstWaiter节点,移动到同步队列的最后一个节点那。并且把刚刚移动到的那根节点后面(移动前的最后一个节点,移动后的倒数第二节点)的waitState属性设置为-1(0是默认。-1是signal 信号,cancelled 作废 是1,conditoin 条件 是-2,propagate 传播 是-3)。

三、aqs的实现

reentrantLock有lock、unlock;reentrantLock的conditoin有await和signal。
ReentreanLock lock = new ReentrantLock();
Condition c = lock.newCondition();
lock.lock(); 走的是aqs的acquire操作
lock.unlock(); 走的是aqs的release操作
c.await(); 类似于wait 是用aqs的条件队列来实现的
c.signal(); 类似于notify 是用aqs的条件队列来实现的
c.sianalAll(); 类似于notifyAll

具体实现如下图所示:
在这里插入图片描述

总结

其实对于多线程来说,要想避免同时操作共享资源,势必是需要加锁的,而aqs是绝大多数锁的底层框架,因此熟悉和掌握aqs是我们所必须要掌握的技能。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值