并发编程|说完AQS,面试官为何不淡定了?_面试

你能说下什么是AQS

AQS是队列同步器AbstractQueueSynchronizer的简写,它是用来构建锁和其他同步组件的基础框架,它定义了一个全局的int 型的state变量,通过内置的FIFO(先进先出)队列来完成资源竞争排队的工作。

AQS中使用到了哪些设计模式?

模版设计模式

如何修改和访问同步器的状态?

  • getState:获取当前同步状态
  • setState:设置当前同步状态
  • compareAndSetState:使用CAS设置当前状态,该方法能保证状态设置的原子性。

AQS提供了哪些模版方法?

方法名称

方法说明

Acquire()

独占锁获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用重写的tryAccquire()方法

acquireShared()

获取共享锁,如果当前线程没有获取到共享锁,则会进入到同步等待队列,,他与独占锁不同的是,共享锁能被多个线程同时占有

release

释放同步状态,并且通知同步器,唤醒等待队列中第一个节点中包含的线程。

releaseShare

释放同步状态

上面总共列出了7个模版方法,可以将这7个模版方法归为如下三类:独占锁的获取与释放、共享锁的获取与释放、同步状态的查询与设置

你能说下AQS中同步队列的数据结构么?


幸好我提前刷了面试题,不然就被卡在这里了。 首先来张图,来镇镇场子

并发编程|说完AQS,面试官为何不淡定了?_AQS_02

  • 当前线程获取同步状态失败,同步器将当前线程机等待状态等信息构造成一个Node节点加入队列,放在队尾,同步器重新设置尾节点
  • 加入队列后,会阻塞当前线程
  • 同步状态被释放并且同步器重新设置首节点,同步器唤醒等待队列中第一个节点,让其再次获取同步状态

上面的这个流程,细心的同学应该会发现一个问题,设置首尾节点的时候会不会发生线程安全问题呢?如果会的话应该怎么做呢,我们看下它们是怎么做的

同步器如何设置尾节点,才能保证线程安全呢?

我们先分析下,为什么设置尾节点的时候会出现线程安全呢?


当多个线程同一时刻去获取同步状态(独占锁)的时候,肯定只会有一个线程竞争成功,那么其他线程都会被放到等待队列的末尾,当多个线程同时被塞到队列末尾的时候,就相当于同时竞争末尾这个资源,这时候就会出现线程安全问题了




为了保证设置队尾元素线程安全,同步器提供了**compareAndSetTail(Node expecr,Node update)**方法,他传入当前线程认为的尾节点和当前要设置成尾节点的节点,只有设置成功,才将当前节点正式与之前的尾节点建立关联。
并发编程|说完AQS,面试官为何不淡定了?_线程安全_03

同步器如何设置首节点的时候,是不是也要用cas来保证线程安全呢?面试官当时很邪魅的在笑


当时第一反应,当然咯,但是在大脑飞速运转之后,回想起来昨天晚上刷面试题的时候,好像刷到了这个题,然后推口而出:当然不会咯,面试官的笑容逐渐消失,当即问我,为什么不会?我是这样回答的


首先 同步器设置尾节点的时候需要cas保证线程安全性是因为设置尾节点的时候是存在多个线程同时竞争设置的,但是设置尾节点的时候是不会存在多个线程同时竞争去设置的。

因为,释放同步状态设置头节点的时候,只有获取到同步状态的线程才能设置,能够获取到独占锁的同步状态,当然只有一个线程啦,所以这里是不可能发生线程安全性的问题,那么就不需要使用CAS保证线程安全的问题了,直接断开之前的首节点,将下一个节点设置成首节点

并发编程|说完AQS,面试官为何不淡定了?_AQS_04

独占式同步状态的获取与释放的源码你有了解么?


我内心的独白是,这个入我早已了然于胸了

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

哈哈,虽然只有几行代码,但是它却完成了同步锁获取的整个过程,你还别不信,我来和你娓娓道来

  • 当前线程调用自定义同步器实现的tryAcquire方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则执行后续流程
  • 构造独占式同步节点,通过调用addWaiter方法将Node塞入同步队列尾部,并且调用acquireQueued方法自旋获取同步锁状态,如果获取不到,则阻塞当前线程。
private Node addWaiter(Node 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(node);
}

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

上面两段代码第一次看会有点晕,乐哉也是的,当时结合上面分析过的流程来看,感觉就会清晰很多,我直接画个思维导图吧

并发编程|说完AQS,面试官为何不淡定了?_多线程_05

我们歇一会,继续分析acquireQueued 方法,看看它里面都做了什么呢?

final boolean acquireQueued(final Node node,int arg){
boolean failed = true;
try{
boolean interrupted = false;
for(;;){
final Node p = node.predcessor();
if(p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
failed = false;
return interrupted;
}
}
}
}

分析上面的代码片段,可以得出acquireQueued内部使用自旋的方式获取同步状态,并且只有 当前节点的前驱节点是头节点,才会尝试调用tryAcquire 获取同步状态,否则继续自旋。面试官看到我说到了这里,就又插了一句说:
**为什么只能其前驱节点是头节点,才会尝试获取同步状态呢?**我真想来一句,我这不是快要说了么!!(脾气爆得狠)
并发编程|说完AQS,面试官为何不淡定了?_AQS_06

并发编程|说完AQS,面试官为何不淡定了?_AQS_07

为什么只能其前驱节点是头节点,才会尝试获取同步

当拥有同步状态的线程释放同步状态的时候,会唤醒其后继节点。为了维护FIFO原则,其后继节点被唤醒后是需要检查自己的前驱节点是不是头节点,这样才能保证FIFO原则。独占锁的释放我就不说了,这里用一个流程图来汇总下我说内容
并发编程|说完AQS,面试官为何不淡定了?_线程安全_08

你前面一直在说独占锁和共享锁,你来说说他们之间有什么区别呢?

老师,我给你画个图吧,画画个北北,画画个北北。。。突然唱起来了,好尴尬啊
并发编程|说完AQS,面试官为何不淡定了?_多线程_09

面试官回答说:这个是他们概念上的区别,你能说说AQS在代码实现,这两种的区别么?
从面试官的回答来看,他其实就是想试探下你有没有深入的去了解AQS的源码,因为我是看过的啊,于是我又继续和他侃大山了。

  • 区别1:共享锁是可以被多个线程同时拥有,并且获取同步状态是否成功,共享锁是通过判断返回值是否大于0,而独占锁是通过true或false来判断
  • 区别2:独占锁在释放同步状态的时候是不用关心线程安全的问题,因为只有一个线程在释放同步状态,但是共享锁是被多个线程同时拥有,所以释放同步状态的时候需要保证线程安全,一般通过CAS+自旋来实现

面试官最后丢了一句,啥时候入职啊,此刻我的内心是这样的
并发编程|说完AQS,面试官为何不淡定了?_面试_10

今天分享的面试内容到此结束,我们下一期再见吧。

并发编程|说完AQS,面试官为何不淡定了?_java_11

微信搜一搜【乐哉开讲】关注帅气的我,回复【干货】,将会有大量面试资料和架构师必看书籍等你挑选,包括java基础、java并发、微服务、中间件等更多资料等你来取哦。
书读的越多而不加思考,你就会觉得你知道得很多;而当你读书而思考得越多的时候,你就会越清楚地看到,你知道得很少。——伏尔泰