免责声明: 这是本人第一个原创系列,希望以轻松幽默的风格讲解代码的实现原理,更因为本人水平有限,难免有疏漏的地方。如果读者遇到文章中需要改进或者看不懂,甚至是觉得错误的地方,可以给我留言。
关于synchronized
天梯之路的更新结束了,本来想先结束缓一缓的。没办法当初答应了大家的东西就一定要发出去,但是总觉得还缺少了什么,因为我给自己的这个系列定义就是面向高级的,所以特别简单的东西就没有多费笔墨,基本就提一嘴,所以也导致了钻石和王者篇内容过于硬核,阅读量少了很多(说的好像,前面两篇阅读量很多一样?),很可能大部分人都不会看完,更别说认认真真看完了,这时候脑子里有一个声音会跟我说,要么做点迎合大部分人口味的科普?不!至少现在的我不愿意做这些别人早就做过的内容,可能随手百度下就能搜到大量相关的博文教程或视频,我就想做别人没做过的,硬核的技术文章,就算我写的东西没有人看,也在互联网上留下了点东西,以后出去面试吹牛也是可以吹上两句的。
系列往期回顾-青铜
系列往期回顾-黄金
系列往期回顾-钻石
系列往期回顾-王者
补充
在说今天的面试题之前,我之前的几篇都没有提到的一个话题就是锁的降级,其实可以搜到外面的很多文章都会说,锁是不会降级的。我的确没有在synchronized
的流程中有发现锁降级的相关代码。不过,我实验下来却不是这个结果,在锁对象膨胀成重量级锁之后退出了同步代码块,如果立马打印锁对象头的话,是看到mw
仍然是重量级锁的状态,但是当sleep
一秒以后,再打印锁对象可以看到就变回了无锁状态。所以我觉得锁是有降级的。下面是demo
public class JOL4Test {
private static Object o = new Object();
private static class T extends Thread {
@Override
public void run() {
synchronized (o) {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
print(o);
new T().start();
synchronized (o) {
System.out.println("In main sync");
print(o);
}
print(o);
Thread.sleep(1000L);
System.out.println("after sleep");
print(o);
}
private static void print(Object object) {
System.out.println(ClassLayout.parseInstance(object).toPrintable());
System.out.println("======================================================");
}
}
回归正题,本期的补完篇就整理下关于synchronized
的面试题,今天以下的答案都是我个人认为的正确答案,仅供参考。
1.请描述synchronized和Reentranlock的底层实现及重入的底层原理
这个应该是大部分面试官会问的问题了,首先可以先聊聊两者的相同不同之处。相同:
- 1.都支持可重入。
- 2.默认都是非公平锁。
- 3.底层都用到了
CAS
,链表
不同:
- 0.
Reentranlock
是Java
实现的,synchronized
是关键字由字节码实现。 - 1.
synchronized
没有公平锁,而Reentranlock
有公平实现。 - 2.
Reentranlock
可以创建不同的Condition
管理不同情况下的,线程通信。 - 3.
Reentranlock
需要显式的释放锁,特别要注意异常情况,synchronized
不需要显式的释放锁。
Reentranlock简单的原理
Reentranlock
内部使用AQS
,而AQS
有一个相当重要的state
字段,所谓的争抢锁,就是看哪个线程可以把state
字段使用CAS
的方式,把它从0
改到1
就视为获取到该锁,而没有获取到锁的线程会进入AQS
中的双向链表队列挂起,等待锁释放后的唤醒。
synchronized简单的原理
看了我前面的介绍应该知道synchronized
有4种锁:
- 偏向锁:通过
CAS
设置当前线程ID至锁对象的mw
来获取锁,有线程竞争就会膨胀至重量级锁。 - 轻量级锁:将
mw
记录至栈帧中,再使用CAS
设置该栈帧指针至锁对象的mw
成功便获取锁,有线程竞争会膨胀至重量级锁。 - 重量级锁:会创建一个
Monitor
对象并存入mw
中,通过CAS
设置Monitor
对象的onwer
字段来获取锁,失败的线程会先进行自旋等待,自旋获取锁失败同样会通过链表的形式进入队列被挂起,等待锁释放后的唤醒。
Reentranlock的重入原理
获得锁的线程通过把state
加1
即可,释放一次减1
,减至0
视为完全释放。
synchronized的重入原理
- 偏向锁 | 轻量级锁:通过压一个栈帧的方式去计数重入锁次数,释放锁便弹出一个栈帧,全部弹出后视为完全释放。
- 重量级锁:使用一个
recursions
字段去进行计数,原理和Reentranlock
的state
一样,不再赘述。
2.自旋锁一定比重量级锁效率高吗?
这种题目中有一定的字眼的答案肯定是不一定!但是面试官肯定不是问你这个答案,重要的是背后的理解。看过之前的文章的朋友一定听过我反复强调过,自旋锁本身就是重量级锁的一部分,但是为了避免这里和面试官咬文嚼字,留下不好的印象(除非你就是想和他刚正面),理解到意思就行了,点到为止,点到为止。先把这两种锁解释一下:
- 1.自旋锁是通过自旋(循环)的方式等待在原地去尝试获取锁。
- 2.题目中的重量级锁,指的操作系统层面的互斥锁,需要线程从用户态切换到内核态,消耗巨大。
解释完以上的内容,答案就显而易见了,因为自旋锁并不一定能获取到锁,而且会占用CPU
的资源,如果自旋到最后还是逃避不了被挂起的命运,简直血亏,还不如上来直接就被挂起来的痛快,所以自旋锁适合同步代码块比较短小的时候,如果同步代码块的业务逻辑特别长,那就不应该自旋直接去挂起更好。
3.打开偏向锁是否效率一定会提升?为什么?
答案同上,不一定!如果有看过钻石篇的朋友们,一定知道偏向锁的逻辑简直比轻量级锁和重量级锁加起来都要复杂,而且偏向锁无法应对线程竞争,一旦有竞争就会进行偏向锁撤销,之后再是膨胀流程,如果撤销到一定次数,还会分别进行,批量重偏向,批量锁撤销的流程,再加上这些流程都必须等到safepoint
才能进行的(原理就不赘述了),所以导致如果频繁的撤销膨胀会消耗巨大性能,所以不如直接关闭偏向锁-XX:-UseBiasedLocking
,直接让他膨胀成重量级锁。甚至可以直接使用-XX:+UseHeavyMonitors
,只启用重量级锁。
4.请描述锁的四种状态和升级过程
4种状态是:无锁、偏向锁、轻量级锁、重量级锁。如果再细分的话:无锁(无hash)、无锁(有hash)、偏向锁(无锁)、偏向锁(有锁)、轻量级锁、重量级锁。升级过程我在之前的文章中也反复提到了:
- 无锁 遇到
synchronized
升级成 轻量级锁 遇到线程竞争 升级成 重量级锁。 - 偏向锁(无锁) 遇到
synchronized
升级成 偏向锁(有锁) 遇到线程竞争 升级成 重量级锁。
如果细分领域的话:
- 无锁(无hash) 遇到
synchronized
升级成 轻量级锁 遇到线程竞争 升级成 重量级锁。 - 无锁(有hash) 遇到
synchronized
升级成 轻量级锁 遇到线程竞争 升级成 重量级锁。 - 偏向锁(无锁) 遇到
synchronized
升级成 偏向锁(有锁) 遇到需使用hashcode
的方法 升级成 重量级锁。原因很简答,因为偏向锁和轻量级锁都没有地方去存放这个临时才决定生成的hashcode
,只有重量级锁才能搞定。
5.同时访问synchronized的静态和非静态方法,能保证线程安全吗?
不能,因为锁对象不同,使用的不是同一把锁。
6.父类的方法生命了synchronized,子类重写该方法,但是不声明synchronized,是线程安全的吗?
不是,synchronized
不能继承,需要的话,得自己声明。
7.在具体开发中,是使用synchronized修饰方法多,还是代码块多?哪个比较好?
肯定代码块比较好,颗粒度容易控制,而且每一个同步方法,都有一个等效的同步块的写法。
网上搜了些面试题,很多问的都大同小异,我上面基本也都覆盖到了。大家有什么想问的,直接写在评论里,我会回复的。
好了,synchronized
系列先告一段落了,以后有什么补充的话,再以精简的短文方式来叙述了。下一个硬核的系列,我大概也已经想好方向了,最近工作比较忙,可能要鸽一段时间了。
最后,再喊出我们的口号:拒绝背书,加油,奥利给!