一线大厂的面试通常会就一个点进行追问深挖,将很多求职者追问到语尽词穷的地步。因此常常被诟病为面试造火箭,工作拧螺丝。这跟大厂的选人用人策略有很大关系,也是不同公司人才观的一种体现。今天就跟大家分享一道简单的面试题。前面的文章中我们已经介绍了关于syncronized的原理,相信看过的同学对锁的问题应该会有比较深刻的认识。今天就接着syncronzied的主题分享一道常见的面试题。
面试官S:OK,刚刚听了你关于syncronized的介绍,感觉理解还是比较全面的,那么除了syncronized我们平常还用过别的方法来实现线程同步么?
程序员Q:有的,我们项目中常用的还有并发包中的ReentrantLock类。
面试官S:简单谈一下ReentrantLock与syncronized有什么不一样的?
程序员Q:首先从实现层面上syncronized是Java的关键字,是JVM实现提供的互斥机制,而ReentrantLock是JDK并发包提供的类,从这个方面来说这两个是不同层级的东西。syncronized是JVM的一部分,而ReentrantLock是构建在Java语言基础上的实现类。从使用上来说,syncronized使用更简单只需要将需要同步的逻辑(类,对象,代码块)用关键字包裹起来就可以了。而ReentrantLock使用起来相对麻烦一点,需要声明对象,然后在需要同步的逻辑前后分别调用lock和unlock接口。从功能上来说syncronized关键字只支持非公平锁,而ReentrantLock支持公平和非公平锁。
面试官S: OK,刚刚你说提到,ReentrantLock是构件在Java语言的基础上,那么相关的实现原理有了解过么?
程序员Q:有的,ReentrantLock是构建在AQS(AbstractQueuedSynchronizer)的基础上的实现。AQS是Java并发包中线程同步的基础类,他提供了一个通用的线程的同步模型,用户在此基础上可以方便的构建线程同步数据结构,并发包中的很多数据结构都是在此基础上派生出来的。简单来说,AbstractQueuedSynchronizer通过状态标志state,线程排队队列,线程竞争原子操作CAS搭建了一套线程同步的实现框架,派生类通过这个框架可以搭建特定场景的线程同步类,当然Java并发包中已经内置了很多实现供用户直接使用,如ReentrantLock,CountDowLatch,CyclicBarrier,Semaphore等等。
面试官S:OK,刚刚你提到了ReentrantLock是基于AQS实现的,能够详细的谈谈这边的实现细节么?
程序员Q:好的。这个过程跟我们刚刚聊过的syncronized的实现原理很相似。这边有三个比较重要的成员,状态变量state,锁持有线程thread和等待队列queue。我来画一张图表示下这个过程:
整个抢占过程可以分为以下几个步骤:
-
线程lock调用对state成员进行CAS(CompareAndSet)将state从0设置为1。如果成功则抢占成功,将thread成员指向自己。
-
如果失败则说明有线程占有锁,检查thread是否指向自己,如果指向自己则将state加1。否则线程挂起(通过LockSurpport的park接口)进入等待队列。
-
当持有锁的线程释放锁后,从等待队列中唤醒头部节点,进行抢占。
-
抢占成功后将thread指向自己,同时移除队列节点。
这就是抢占的整个过程,在抢占过程中的队列操作都是通过CAS来处理多线程同步问题的。
面试官S:嗯,那么你之前提到ReentrantLock能够支持公平锁和非公平锁,你刚刚的描述里面好像没有涉及,能补充一下么?
程序员Q:额,好的。公平非公平主要体现在线程第一次抢占时。非公平模式下,线程调用lock就会立刻进行一次CAS抢占。此时线程有一定的概率成功(与队列中的唤醒线程进行PK)。而在公平锁模式下,线程会检查当前队列中有没有等待线程,如果有存量等待线程,则不进行CAS抢占,直接进入队列进行排队。这就是公平和非公平的差异。
面试官S:嗯,不错。那么lock对象被线程抢占成功后会设置state变量的值。此时如何保证其他线程能够立刻获取到最新的state的。我们的线程是自带工作内存的,CPU也是有高速缓存的。
程序员Q:这个只需要通过volatile关键字就可以,他能够保证多线程变量的可见性。
到此关于ReentrantLock和AQS的追问就告一段落。大部分科技公司通过这种与候选人进行实现细节的探索,可以对候选人的技术深度以及技术热情进行挖掘。从而寻找到具备专家潜力的程序员。从我的经验来说,大厂对候选人的考察维度一般从三个方面展开,技术广度,技术深度和个人潜力。其中个人潜力占比很大,甚至高于前面两个部分,有时候甚至具备一票否决权。
欢迎关注: