并发编程核心底层AQS你知道多少
一、你知道AQS吗?核心思想是什么?干嘛用的?
二、源码看下重要的几个方法以及流程步骤
三、你知道的AQS有几种同步方式,实现同步器一般要覆盖哪些方法
面试官:AQS你知道的吧,简单介绍一下
面试的时候被面试官这么猛的一问,支支吾吾半天没有很清晰的思路来做一个很好的回答,本博文重点针对AQS是什么,干什么用的,核心思想是什么,加锁/解锁的步骤是哪些、实现同步器的话哪些核心方法是必不可少的进行了描述,要耐心看完呀~
一、你知道AQS吗?核心思想是什么?干嘛用的?
AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。它是一个Java提高的底层同步工具类,虽然我们不会直接使用这个类,但是这个类是Java很多并发工具的底层实现,比如CountDownLatch、ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的
就好比我们写代码,有很多框架,我们会对框架进行二次开发实现,就能写出很多的网站啊系统之类的出来,AQS就好比这种框架,不用考虑那么多底层的东西,直接用它提供的接口就行了,按照它的规范去重写对应的方法。
简单来说:是用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态对象
一个是 state(用于计数器,类似gc的回收计数器)
一个是线程标记(当前线程是谁加锁的),
一个是阻塞队列(用于存放其他未拿到锁的线程)举例;线程A调用了lock()方法,通过CAS将state赋值为1,然后将该锁标记为线程A加锁。如果线程A还未释放锁时,线程B来请求,会查询锁标记的状态,因为当前的锁标记为 线程A,线程B未能匹配上,所以线程B会加入阻塞队列,直到线程A触发了 unlock() 方法,这时线程B才有机会去拿到锁,但是不一定肯定拿到
二、源码看下重要的几个方法以及流程步骤
先看下这张图
首先从类的定义开始逐步的深入了解:
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { //state变量表示锁的状态,0表示未锁定 大于0表示已锁定,这个值可以用来实现锁的【可重入性】 //同时这个变量还是用volatile关键字修饰的,保证可见性 private volatile int state; //等待队列的头节点,只能通过setHead方法修改,如果head存在,能保证waitStatus状态不为CANCELLED private transient volatile Node head; // 等待队列的尾结点,只能通过enq方法来添加新的等待节点 private transient volatile Node tail;}
acquire(int arg) 源码分析,好比加锁lock操作。成功后就直接返回,失败后就通过addWaiter方 法把当前线程封装成一个Node,加到队列的尾部,再通过acquireQueued方法尝试获取同步锁, 成功获取锁的线程的Node节点会被移出队列。
注意: 线程获取锁成功后 直接返回,不会进入等待队列里面,只有失败的时候才会
//该方法主要调用tryAcquire方法尝试获取锁,成功返回true,失败就将线程封装成Node对象,放入队列。public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //如果以上条件都满足,会执行selfInterrupt方法中断当前线程。 selfInterrupt();}
tryAcquire()尝试直接去获取资源,如果成功则直接返回,AQS里面未实现但没有定义成abstract,因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared,类似设计模式里面的适配器模式
//在此处是空实现,因为AQS可以构造很多种锁,比如独占锁,共享锁,这个tryAcquire 是基于独占锁。用了适配器模式protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
addWaiter() 根据不同模式将线程加入等待队列的尾部,有Node.EXCLUSIVE互斥模式、Node.SHARED共享模式;如果队列不为空,则以通过compareAndSetTail方法以CAS将当前线程节点加入到等待队列的末尾。否则通过enq(node)方法初始化一个等待队列
// 1、Node.EXCLUSIVE:独占模式 // 2、Node.SHARED:共享模式 private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // 尝试快速添加尾结点,失败就执行enq方法 Node pred = tail; if (pred != null) { node.prev = pred; // CAS的方式设置尾结点 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //通过enq(node)方法初始化一个等待队列 enq(node); return node; }
acquireQueued()使线程在等待队列中获取资源,一直获取到资源后才返回,如果在等待过程中被中断,则返回true,否则返回false
//获取失败则将当前线程封装为Node.EXCLUSIVE的Node节点插入AQS阻塞队列的尾部
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); } }
加锁操作看完后,我们再来看下解锁的操作
release(int arg) 好比解锁unlock
独占模式下线程释放指定量的资源,里面是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了;在自义定同步器在实现时,如果已经彻底释放资源(state=0),要返回true,否则返回false
public final boolean release(int arg) { // 尝试释放锁 if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) // unparkSuccessor方法用于唤醒等待队列中下一个线程 unparkSuccessor(h); return true; } return false; }
三、你知道的AQS有几种同步方式,实现同步器一般要覆盖哪些方法
独占式: 比如ReentrantLock共享式:比如Semaphore存在组合:组合式的如ReentrantReadWriteLock,AQS为使用提供了底层支撑,使用者可以自由组装实现1. boolean tryAcquire(int arg) //独占模式2. boolean tryRelease(int arg) //独占模式3. int tryAcquireShared(int arg) //共享模式4. boolean tryReleaseShared(int arg) //共享模式5. boolean isHeldExclusively() //判断是哪种模式不需要全部实现,根据获取的锁的种类可以选择实现不同的方法,比如实现支持独占锁的同步器应该实现tryAcquire、 tryRelease、isHeldExclusively实现支持共享获取的同步器应该实现tryAcquireShared、tryReleaseShared、isHeldExclusively