前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >聊完AQS,面试官为何对我竖起大拇指?

聊完AQS,面试官为何对我竖起大拇指?

作者头像
AI码师
发布2020-11-19 16:02:24
2.7K0
发布2020-11-19 16:02:24
举报

兄得我今天分享一次有关AQS的面试经历,如有雷同,咱们可能是同一个面试官,瓜子、板凳、啤酒、花生米备好了,咱们正式开始。

你能说一说什么是AQS吗?

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

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

模版设计模式

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

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

AQS提供了哪些模版方法?

方法名称

方法说明

Acquire()

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

acquireShared()

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

release

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

releaseShare

释放同步状态

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

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

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

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

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

接下来,我们看下它们是怎么做的:

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

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

  • 当多个线程同一时刻去获取同步状态(独占锁)的时候,肯定只会有一个线程竞争成功,那么其他线程都会被放到等待队列的末尾,当多个线程同时被塞到队列末尾的时候,就相当于同时竞争末尾这个资源,这时候就会出现线程安全问题了
  • 为了保证设置队尾元素线程安全,同步器提供了compareAndSetTail(Node expecr,Node update)方法,他传入当前线程认为的尾节点和当前要设置成尾节点的节点,只有设置成功,才将当前节点正式与之前的尾节点建立关联。

同步器如何设置首节点的时候,是不是也要用cas来保证线程安全呢?

“面试官脸上的笑容要多邪媚有多邪魅“

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

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

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

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

代码语言:javascript
复制
public final void acquire(int arg) {
        if(!tryAcquire(arg)
  &&acquireQueued(addWaiter(Node.EXCLUSIVE),arg))
        seleInterrupt();
        }
}

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

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

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

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

代码语言:javascript
复制
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 获取同步状态,否则继续自旋。面试官看到我说到了这里,就又插了一句说:

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

真想来一句,这不快要说了么!!(暴脾气)

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

当拥有同步状态的线程释放同步状态的时候,会唤醒其后继节点。为了维护FIFO原则,其后继节点被唤醒后需要检查自己的前驱节点是不是头节点,这样才能保证FIFO原则。独占锁的释放我就不说了,这里用一个流程图来汇总下我说内容

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

老师,我给你画个图吧,画画个北北,画画个北北。。。突然唱起来了,面试官一脸瞪着我,好尴尬啊。

面试官回答说:这个是他们概念上的区别

你能说说AQS在代码实现,这两种的区别么?

从面试官的回答来看,他其实就是想试探下你有没有深入的去了解AQS的源码,因为我是看过的啊,于是我又继续和他侃大山了。

  • 区别1:共享锁是可以被多个线程同时拥有,并且获取同步状态是否成功,共享锁是通过判断返回值是否大于0,而独占锁是通过true或false来判断
  • 区别2:独占锁在释放同步状态的时候是不用关心线程安全的问题,因为只有一个线程在释放同步状态,但是共享锁是被多个线程同时拥有,所以释放同步状态的时候需要保证线程安全,一般通过CAS+自旋来实现
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-09-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 乐哉开讲 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 你能说一说什么是AQS吗?
  • AQS中使用到了哪些设计模式?
  • 如何修改和访问同步器的状态?
  • AQS提供了哪些模版方法?
  • 上面总共列出了7个模版方法,可以将这7个模版方法归为如下三类:独占锁的获取与释放、共享锁的获取与释放、同步状态的查询与设置
    • 你能说下AQS中同步队列的数据结构么?
      • 同步器如何设置尾节点,才能保证线程安全呢?
        • “面试官脸上的笑容要多邪媚有多邪魅“
          • 你前面一直在说独占锁和共享锁,你来说说他们之间有什么区别?
            • 你能说说AQS在代码实现,这两种的区别么?
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档