一个工作4年的程序员,简历上写精通并发编程,并且阅读过AQS(AbstractQueuedSynchronizer)
的源码,然后面试官只问了他一个问题,然后就垮了!
AQS大家都不陌生,是J.U.C包里面一个非常重要的线程同步器。
面试官提了这样一个问题:“AQS为什么要采用双向链表结构”?
关于这个问题的回答,我整理到了一个10W字的面试文档里面,大家可以扫描文章尾端二维码领取。
下面看看高手的回答。
大家记得点赞、收藏加关注!
需要高手面试文档面试文档的小伙伴可以扫描文章底部二维码
高手:
首先,双向链表的特点是它有两个指针,一个指针指向前置节点,一个指针指向后继节点。
所以,双向链表可以支持 常量O(1) 时间复杂度的情况下找到前驱结点,基于这样的特点。
双向链表在插入和删除操作的时候,要比单向链表简单、高效。
因此,从双向链表的特性来看,我认为AQS使用双向链表有三个方面的考虑。
-
第一个方面,没有竞争到锁的线程加入到阻塞队列,并且阻塞等待的前提是,当前线程所在节点的前置节点是正常状态,
这样设计是为了避免链表中存在异常线程导致无法唤醒后续线程的问题。
所以线程阻塞之前需要判断前置节点的状态,如果没有指针指向前置节点,就需要从head节点开始遍历,性能非常低。
-
第二个方面,在Lock接口里面有一个,lockInterruptibly()方法,这个方法表示处于锁阻塞的线程允许被中断。
也就是说,没有竞争到锁的线程加入到同步队列等待以后,是允许外部线程通过interrupt()方法触发唤醒并中断的。
这个时候,被中断的线程的状态会修改成CANCELLED。
被标记为CANCELLED状态的线程,是不需要去竞争锁的,但是它仍然存在于双向链表里面。
意味着在后续的锁竞争中,需要把这个节点从链表里面移除,否则会导致锁阻塞的线程无法被正常唤醒。
在这种情况下,如果是单向链表,就需要从Head节点开始往下逐个遍历,找到并移除异常状态的节点。
同样效率也比较低,还会导致锁唤醒的操作和遍历操作之间的竞争。
-
第三个方面,为了避免线程阻塞和唤醒的开销,所以刚加入到链表的线程,首先会通过自旋的方式尝试去竞争锁。
但是实际上按照公平锁的设计,只有头节点的下一个节点才有必要去竞争锁,后续的节点竞争锁的意义不大。
否则,就会造成羊群效应,也就是大量的线程在阻塞之前尝试去竞争锁带来比较大的性能开销。
所以为了避免这个问题,加入到链表中的节点在尝试竞争锁之前,需要判断前置节点是不是头节点,如果不是头节点,就没必要再去触发锁竞争的动作。
所以这里会涉及到前置节点的查找,如果是单向链表,那么这个功能的实现会非常复杂。
总结
关于这个问题,99%的人都回答不上来。
而且我简单翻了一些技术博客,基本上全都是错的。
对AQS理解不深刻的情况下,乱回答,导致很多同学被误解。
理解一个技术为什么这么设计,关键在于它需要解决什么样的问题。
另外,我将所有Java面试系列制作成了完整的面试文档。它的便捷之处在于,可以通过检索的方式,找到你想要的面试题,目前已经更新180期,总计超过15W字!