AQS的基本原理

AQS的定义

AQS 是⼀个⽤来构建锁和同步器的框架,使⽤ AQS 能简单且⾼效地构造出应⽤⼴泛的⼤量的同

步器,⽐如我们提到的 ReentrantLock ,countdownlatch cyclicbarrier

AQS 核⼼思想

如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的⼯作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占⽤,那么就需要⼀套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是⽤ CLH 队列锁实现的,即将暂时获取不到锁的线程加⼊到队列中。

AQS 对资源的共享⽅式

AQS 定义两种资源共享⽅式

Exclusive(独占):只有⼀个线程能执⾏,如 ReentrantLock 。⼜可分为公平锁和⾮公平

锁:

公平锁:按照线程在队列中的排队顺序,先到者先拿到锁

⾮公平锁:当线程要获取锁时,⽆视队列顺序直接去抢锁,谁抢到就是谁的

Share(共享):多个线程可同时执⾏,如

CountDownLatch 、 Semaphore 、 CountDownLatch 、 CyclicBarrier 、 ReadWriteLock

AQS用state 变量来表示同步状态 用volatile 修饰的 可以保证可见性  不保证原子性,所以用CAS来修改变量

不同的⾃定义同步器争⽤共享资源的⽅式也不同。⾃定义同步器在实现时只需要实现共享资源

state 的获取与释放⽅式即可,⾄于具体线程等待队列的维护(如获取资源失败⼊队/唤醒出队

等),AQS 已经在顶层实现好了。

AQS 底层使⽤了模板⽅法模式

同步器的设计是基于模板⽅法模式的,如果需要⾃定义同步器⼀般的⽅式是这样(模板⽅法模式
很经典的⼀个应⽤):
1. 使⽤者继承 AbstractQueuedSynchronizer 并重写指定的⽅法。(这些重写⽅法很简单,⽆⾮
是对于共享资源 state 的获取和释放)
2. AQS 组合在⾃定义同步组件的实现中,并调⽤其模板⽅法,⽽这些模板⽅法会调⽤使⽤
者重写的⽅法。
这和我们以往通过实现接⼝的⽅式有很⼤区别,这是模板⽅法模式很经典的⼀个运⽤。
AQS 使⽤了模板⽅法模式,⾃定义同步器时需要重写下⾯⼏个 AQS 提供的模板⽅法:
isHeldExclusively()//该线程是否正在独占资源。只有⽤到condition才需要去实现它。
tryAcquire(int)//独占⽅式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占⽅式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享⽅式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可⽤资
源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享⽅式。尝试释放资源,成功则返回true,失败则返回false。
AQS 类中的其他⽅法都是 final ,所以⽆法被其他类修改,只有这⼏个⽅法可以被其他类使⽤。
ReentrantLock 为例, state 初始化为 0 ,表示未锁定状态。 A 线程 lock() 时,会调⽤
tryAcquire() 独占该锁并将 state+1 。此后,其他线程再 tryAcquire() 时就会失败,直到 A 线程
unlock() state=0 (即释放锁)为⽌,其它线程才有机会获取该锁。当然,释放锁之前, A 线程
⾃⼰是可以重复获取此锁的( state 会累加),这就是可重⼊的概念。但要注意,获取多少次就
要释放多么次,这样才能保证 state 是能回到零态的。
再以 CountDownLatch 以例,任务分为 N 个⼦线程去执⾏, state 也初始化为 N (注意 N 要与线
程个数⼀致)。这 N 个⼦线程是并⾏执⾏的,每个⼦线程执⾏完后 countDown() ⼀次, state
CAS(Compare and Swap) 1 。等到所有⼦线程都执⾏完后 ( state=0) ,会 unpark() 主调⽤线
程,然后主调⽤线程就会从 await() 函数返回,继续后余动作。
⼀般来说,⾃定义同步器要么是独占⽅法,要么是共享⽅式,他们也只需实现 tryAcquire
tryRelease tryAcquireShared-tryReleaseShared 中的⼀种即可。但 AQS 也⽀持⾃定义同步器同时
实现独占和共享两种⽅式,如 ReentrantReadWriteLock

ReetrantLock源码流程

AQS 内部维护了一个 FIFO(先进先出)的双向队列。它的内部是用双向链表来实现的,由Node组成(

Node 包含了

//节点被取消的状态是不可逆的,也就是说此节点会一直停留在取消状态,不会转变。 static final int CANCELLED = 1; //说明后继节点的线程被 park 阻塞,因此当前线程需要在释放锁或者被取消时,唤醒后继节点 static final int SIGNAL = -1; //说明线程在 condition 条件队列等待 static final int CONDITION = -2; //在共享模式中用,表明下一个共享线程应该无条件传播 static final int PROPAGATE = -3;

//当前线程的等待状态,除了以上四种值,还有一个值 0 为初始化状态(条件队列的节点除外)。 //注意这个值修改时是通过 CAS ,以保证线程安全。 volatile int waitStatus;

//前驱节点 volatile Node prev;

//后继节点 volatile Node next;

//当前节点中的线程,通过构造函数初始化,出队时会置空(这个后续说,重点强调) volatile Thread thread;

另外,在 AQS 类中,还会记录同步队列的头结点和尾结点:tail head

每个数据节点中都包含了当前节点的线程信息,还有它的前后两个指针,分别指向前驱节点和后继节点。每个节点会有一个线程出队时会置空

独占锁

ReetrantLock源码流程

首先,我们从 ReentrantLock 开始分析,它有两个构造方法,一个构造,可以传入一个 boolean 类型的参数,表明是用公平锁还是非公平锁模式。(true是公平说)另一个构造方法,不传入任何参数,则默认用非公平锁。

NonfairSync 和 FairSync 都继承自 Sync ,它们都是 ReentranLock 的内部类。而Sync 类又继承自 AQS (AbstractQueuedSynchronizer)。

对于 NonfairSync.lock

调用lock方法之后 首先用CAS快速尝试获取锁就是把state从0设置为1, 如果失败了 则执行AQS里的acquire 方法 是一个模板  他会调用我们重写的方法

final void lock() {

//通过 CAS 操作把 state 设置为 1

if (compareAndSetState(0, 1))

//如果设值成功,说明加锁成功,保存当前获得锁的线程 setExclusiveOwnerThread(Thread.currentThread());

else //如果加锁失败,则执行 AQS 的acquire 方法

acquire(1);

}

public final void acquire(int arg) {

if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }

  1. 通过 tryAcquire 方法,尝试获取锁,如果成功,则返回 true,失败返回 false 。

  2. tryAcquire 失败之后,会先调用 addWaiter 方法,把当前线程封装成 node 节点,加入同步队列(独占模式)。

  3. acquireQueued 方法会把刚加入队列的 node 作为参数,通过自旋去获得锁。

tryacquire具体是 判断state是否为0 为0就用CAS去尝试获取锁 不为0 就判断当前锁是不是被当前线程占用 是就将 state+1(可重入); 否则如果获取锁失败之后,就会调用 addWaiter 方法把当前线程加入同步队列。

addwaiter 把当前线程封装成 Node 尝试CAS快速入队(当尾结点设置为自己 如果成功了 把之前节点的next设置为这个节点),如果失败,则会调用 enq 入队方法

enq 先判断尾节点是否为空,为空说明还没有初始化,先把head节点初始化(new Node),通过自旋,尝试把自己设置为tail节点 入队成功之后,就会调用 acquireQueued 方法自旋抢锁。

 å¾ç

使用 CAS 创建 head 节点的时候只是简单调用了 new Node() 方法,并不像其他节点那样记录 thread,这是为啥

因为 head 结点为虚结点,它只代表当前有线程占用了 state,至于占用 state 的是哪个线程,其实是调用了上文的 setExclusiveOwnerThread(current) ,即记录在 exclusiveOwnerThread 属性里。

 

 

acquireQueued 自旋抢锁。

acquireQueued判断前驱节点是否是 head 节点,是就调用 tryAcquire 方法抢锁

如果成功了 把自己 设为头结点

if (p == head && tryAcquire(arg))

private void setHead(Node node) { head = node; node.thread = null; node.prev = null; }

需要注意的是,这个方法,会把头结点的线程设置为 null 。想一下,为什么?

因为,此时头结点的线程已经抢锁成功,需要出队了。自然的,队列中也就不应该存在这个线程了。

PS:由 enq 方法,还有 setHead 方法,我们可以发现,头结点的线程总是为 null。这是因为,头结点要么是刚初始化的空节点,要么是抢到锁的线程出队了。因此,我们也常常把头结点叫做虚拟节点(不存储任何线程)。

如果不是head 或者tryAcquire失败

if (shouldParkAfterFailedAcquire(p, node) && //线程被挂起时,判断是否被中断过 parkAndCheckInterrupt())

//如果抢锁失败,调用shouldParkAfterFailedAcquire(p, node)

shouldParkAfterFailedAcquire(p, node)根据前驱节点的 waitStatus 状态判断是否需要把当前线程挂起

如果前驱节点状态为signal 为-1 那么前驱节点如果释放了同步状态或者被取消 会通知后继节点

如果是1 Canceller状态 就要一直往前找到一个 值<0的 把他作为自己的前驱节点

然后被挂起-parkAndCheckInterrupt()

当一个线程释放锁的时候会从尾结点往头结点找到一个离当前节点最近的一个有效节点 唤醒之后那个线程就可以继续自旋抢锁(从尾向前获取最后一个非取消状态的结点)

这里的寻找队列的第一个非取消状态的节点为啥要从后往前找呢,因为节点入队并不是原子操作,如下图片

线程自旋时时是先执行 node.pre = pred, 然后再执行 pred.next = node,如果 unparkSuccessor 刚好在这两者之间执行,此时是找不到  head 的后继节点的,如下

图片

LockSupport.unpark(s.thread);

å¾ç

公平锁的需要判断自己的前驱节点是否是头结点 是的话就去CAS抢锁

非公平锁 是队列中所有的没有挂起的节点都会去抢锁(错,只要前面是头结点才能抢锁)

锁的释放

protected final boolean tryRelease(int releases) { //每释放一次锁,state 值就会减 1,因为之前可能有锁的重入 int c = getState() - releases; //如果当前线程不是抢到锁的线程,则抛出异常 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { //只有 state 的值减到 0 的时候,才会全部释放锁 free = true; setExclusiveOwnerThread(null); } setState(c); return free; }

每个lock要对应一个unlock

所以,可以看出来。公平锁是严格按照排队的顺序来的,先来后到嘛,你来的早,就可以早点获取锁。优点是,这样不会造成某个线程等待时间过长,因为大家都是中规中矩的在排队。而缺点呢,就是会频繁的唤起线程,增加 CPU的开销。

非公平锁的优点是吞吐量大,因为有可能正好锁可用,然后线程来了,直接抢到锁了,不用排队了,这样也减少了 CPU 唤醒排队线程的开销。但是,缺点也很明显 会导致线程饥饿

https://mp.weixin.qq.com/s?__biz=MzAwNDA2OTM1Ng==&mid=2453147094&idx=2&sn=ea2a2daa99b0c94874ced4946cd4f175&scene=21#wechat_redirect

  • 2
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值