AQS详解

先来一张总的流程图:

processon原图:

https://www.processon.com/diagraming/5fcf1eec5653bb06f32c5509

 

 

 

AQS流程详解:

AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列(双端双向链表)来完成获取资源线程(线程被封装成一个Node节点)的排队工作,维护了2个指针(head、tail)。

private volatile int state;//共享变量,使用volatile修饰保证线程可见性。

状态信息通过procted类型的getStatesetStatecompareAndSetState进行操作

AQS支持两种同步方式:1.独占式(ReentrantLock)   2.共享式(Semaphore、CountDownLatCh、 CyclicBarrier)

同步器的设计是基于模板方法模式的,一般的使用方式是这样:

  1.使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)

  2.将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。

 

Node节点存放了节点的pre、next、绑定的thread、waitStatus等。

waitStatus:0状态:值为0,代表初始化状态。 waitStatus>0表示取消状态,而waitStatus<0表示有效状态

 

源码流程分析:

·独占式:

acquire-获取锁:

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

a.首先,调用使用者重写的tryAcquire方法,若返回true,意味着获取同步状态成功,后面的逻辑不再执行;若返回false,也就是获取同步状态失败,进入b步骤;

b.此时,获取同步状态失败,构造独占式同步结点,通过addWatiter将此结点添加到同步队列的尾部(此时可能会有多个线程结点试图加入同步队列尾部,需要以线程安全的方式添加);

c.该结点已在队列中尝试获取同步状态,若获取不到,则阻塞结点线程,直到被前驱结点唤醒或者被中断。

addWaiter

为获取同步状态失败的线程,构造成一个Node结点,添加到同步队列尾部;

先cas快速设置,若失败,进入enq方法(enq内部是个死循环,通过CAS设置尾结点,不成功就一直重试,很经典的CAS自旋的用法,这是一种乐观的并发策略。)

acquireQueued方法(会一直阻塞住,死循环去获取锁)

acquireQueued内部也是一个死循环,只有前驱结点是头结点的结点,也就是老二结点,才有机会去tryAcquire;若tryAcquire成功,表示获取同步状态成功,将此结点设置为头结点;若是非老二结点,或者tryAcquire失败,则进入shouldParkAfterFailedAcquire去判断判断当前线程是否应该阻塞,若可以,调用parkAndCheckInterrupt阻塞当前线程,直到被中断或者被前驱结点唤醒。若还不能休息,继续循环。

shouldParkAfterFailedAcquire返回true,也就是当前结点的前驱结点为SIGNAL状态,则意味着当前结点可以放心休息,进入parking状态了。parkAncCheckInterrupt阻塞线程并处理中断。

一句话总结:

a.首先tryAcquire获取同步状态,成功则直接返回;否则,进入下一环节;

b.线程获取同步状态失败,就构造一个结点,加入同步队列中,这个过程要保证线程安全;

c.加入队列中的结点线程进入自旋状态,若是老二结点(即前驱结点为头结点),才有机会尝试去获取同步状态;否则,当其前驱结点的状态为SIGNAL,线程便可安心休息,进入阻塞状态,直到被中断或者被前驱结点唤醒。

在这里插入图片描述

 

release-释放锁

public final boolean release(int arg) {
        if (tryRelease(arg)) {//调用使用者重写的tryRelease方法,若成功,唤醒其后继结点,失败则返回false
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);//唤醒后继结点
            return true;
        }
        return false;
    }
unparkSuccessor:唤醒后继结点 

release的同步状态相对简单,需要找到头结点的后继结点进行唤醒,若后继结点为空或处于CANCEL状态,从后向前遍历找寻一个正常的结点,唤醒其对应线程。

在这里插入图片描述

·共享式

共享式:共享式地获取同步状态。对于独占式同步组件来讲,同一时刻只有一个线程能获取到同步状态,其他线程都得去排队等待,其待重写的尝试获取同步状态的方法tryAcquire返回值为boolean,这很容易理解;对于共享式同步组件来讲,同一时刻可以有多个线程同时获取到同步状态,这也是“共享”的意义所在。其待重写的尝试获取同步状态的方法tryAcquireShared返回值为int。

1.当返回值大于0时,表示获取同步状态成功,同时还有剩余同步状态可供其他线程获取;

2.当返回值等于0时,表示获取同步状态成功,但没有可用同步状态了;

3.当返回值小于0时,表示获取同步状态失败。

大体逻辑与独占式的acquireQueued差距不大,只不过由于是共享式,会有多个线程同时获取到线程,也可能同时释放线程,空出很多同步状态,所以当排队中的老二获取到同步状态,如果还有可用资源,会继续传播下去

在这里插入图片描述

在这里插入图片描述

 

 

reentrantlock公平锁和非公平锁区别:

1、lock方法中,公平锁直接调用acquire方法,而非公平锁会先cas请求一下是否能获取锁,不成功再acquire。

2、acquire调用tryAcquire方法中,获取state如果为0,公平锁会调用hasQueuedPredecessors判断前面是否有排队的节点,如果没有会调用cas尝试获取锁,而非公平锁直接调用cas尝试获取锁。

 

为什么shouldParkAfterFailedAcquire一定要设置前驱节点为SIGNAL:

shouldParkAfterFailedAcquire作用:只有当该节点的前驱结点的状态为SIGNAL时,才可以对该结点所封装的线程进行park操作,否则,将不能进行park操作。

如果前置节点状态为cancel会向前遍历(为什么会用双向链表的原因,向前遍历),将当前节点的pre设置为第一个不为cancel的节点。

因为释放锁的时候如果尝试释放锁成功,会判断头结点是否为SIGNAL状态,如果是才会唤醒head节点的后继节点。

 

为什么初始化队列头节点的时候直接new Node()?

这里可以这样去理解,头节点是不参与排队的,因为它已经获得了同步状态了,那么就说明该头节点的相关线程已经在执行相应的业务逻辑了,而在执行完业务逻辑,释放同步状态后,该头节点是肯定要被垃圾回收的,防止内存空间的浪费,这里就涉及到了gc root,如果对象还有引用的话,垃圾回收器是不会回收它的,所以需要把头节点持有的各种引用都置为null,方便之后的垃圾回收,所以就直接new Node()。

 

Lock接口中lock和trylock区别(以reentrantlock为例):

·lock   获取锁。 如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1。 如果当前线程已经保持该锁,则将保持计数加 1,并且该方法立即返回。 如果该锁被另一个线程保持,则出于线程调度的目的,禁用当前线程,并且在获得锁之前,该线程将一 直处于休眠状态,此时锁保持计数被设置为 1。

·trylock 仅在调用时锁未被另一个线程保持的情况下,才获取该锁。 (会打破公平锁策略) 

1)如果该锁没有被另一个线程保持,并且立即返回 true 值,则将锁的保持计数设置为 1。 即使已将此锁设置为使用公平排序策略,但是调用 tryLock() 仍将 立即获取锁(如果有可用的), 而不管其他线程当前是否正在等待该锁。在某些情况下,此“闯入”行为可能很有用,即使它会打破公 平性也如此。如果希望遵守此锁的公平设置,则使用 tryLock(0, TimeUnit.SECONDS) ,它几乎是等效的(也检测中断)。

2)如果当前线程已经保持此锁,则将保持计数加 1,该方法将返回 true。

3)如果锁被另一个线程保持,则此方法将立即返回 false 值。

4)  也可用于在一段时间内持续获取锁。

指定者: 接口 Lock 中的 tryLock 返回: 如果锁是自由的并且被当前线程获取,或者当前线程已经保持该锁,则返回 true;否则返回 false

 

lock和synchronized区别以及什么时候只能用lock。   

·Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
·Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

什么时候只能用lock:
·lock能够响应中断。synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程,也就无法释放持有的锁A。但如果阻塞状态的线程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能够唤醒它,那它就有机会释放曾经持有的锁 A。这样就破坏了不可抢占条件了。
·支持超时。如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
·非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值