先赞后看,Java进阶一大半
早期sychonrized重量级锁开销大,于是JDK1.5引入了ReentrantLock,包含现在很多偏见都是认为ReentrantLock性能要优于sychonrized。但JDK1.6引入的锁升级,不断迭代,怕是性能往往还优于ReentrantLock。
我是南哥,相信对你通关面试、拿下Offer有所帮助。
敲黑板:本文总结了多线程相关的synchronized、volatile常见的面试题!
面试官:知道可重入锁有哪些吗?
可重入意味着获取锁的粒度是线程而不是调用,如果大家知道这个概念,会更容易理解可重入锁的作用。
既然获取锁的粒度是线程,意味着线程自己是可以获取自己的内部锁的,而如果获取锁的粒度是调用则每次经过同步代码块都需要重新获取锁。
举个例子。线程A获取了某个对象锁,但在线程代码的流程中仍需再次获取该对象锁,此时线程A可以继续执行不需要重新再获取该对象锁。另外线程如果要使用父类的同步方法,由于可重入锁也无需再次获取锁。
在Java中,可重入锁主要有ReentrantLock、synchronized。
面试官:你先说说synchronized的实现原理?
synchronized的实现是基于monitor的。任何对象都有一个monitor与之关联,当monitor被持有后,对象就会处于锁定状态。而在同步代码块的开始位置,在编译期间会被插入monitorenter指令。
当线程执行到monitorenter指令时,就会尝试获取monitor的所有权,如果获取得到则代表获得锁资源。
面试官:那synchronized有什么缺点?
在Java SE 1.6还没有对synchronized进行了各种优化前,很多人都会称synchronized为重量级锁,因为它对资源消耗是比较大的。
面试官:为什么上下文切换要保存当前线程状态?
这就跟读英文课文时查字典一样,我们要先记住课文里的页数,查完字典好根据页数翻到英文课文原来的位置。
同理,CPU要保证可以切换到上一个线程的状态,就需要保存当前线程的状态。
面试官:可以怎么解决synchronized资源消耗吗?
上文我有提到Java SE 1.6对synchronized进行了各种优化,具体的实现是给synchronized引入了锁升级的概念。synchronized锁一共有四种状态,级别从低到高依次是无锁、偏向锁、轻量级锁、重量级锁。
大家思考下,其实多线程环境有着各种不同的场景,同一个锁状态并不能够适应所有的业务场景。而这四种锁状态就是为了适应各种不同场景来使得线程并发的效率最高。
面试官:它们都有什么优缺点呢?
由于每个锁状态都有其不同的优缺点,也意味着有其不同的适应场景。
面试官:重排序知道吧?
指令重排序字面上听起来很高级,但只要理解了并不难掌握。我们先来看看指令重排序究竟有什么作用。
指令重排序的主要作用是可以优化编译器和处理器的执行效率,提高程序性能。例如多条执行顺序不同的指令,可以重排序让轻耗时的指令先执行,从而让出CPU流水线资源供其他指令使用。
但如果指令之间存在着数据依赖关系,则编译器和处理器不会对相关操作进行指令重排序,避免程序执行结果改变。这个规则也称为as-if-serial语义
。例如以下代码。
String book = "JavaGetOffer"; // A
String avator = "思考的陈"; // B
String msg = book + abator; // C
对于A、B,它们之间并没有依赖关系,谁先执行对程序的结果没有任何影响。但C却依赖于A、B,不能出现类似C -> A -> B或C -> B -> A或A -> C -> B或B -> C -> A之类的指令重排,否则程序执行结果将改变。
面试官:那重排序不会有什么问题吗?
在单线程环境下,有as-if-serial语义
的保护,我们无需担心程序执行结果被改变。但在多线程环境下,指令重排序会出现数据不一致的问题。举个多线程的例子方便大家理解。
int number = 0;
boolean flag = false;
public void method1() {
number = 6; // A
flag = true; // B
}
public void method2() {
if (flag) { // C
int i = number * 6; // D
}
}
假如现在有两个线程,线程1执行method1
、线程2执行method2
。因为method1
其中的A、B之间没有数据依赖关系,可能出现B -> A的指令重排序,大家注意这个指令重排序会影响到线程2执行的结果。
当B指令执行后A指令还没有执行number = 6
,此时如果线程2执行method2
同时给i赋值为0 * 6
。很明显程序运行结果和我们预期的并不一致。
面试官:有什么办法可以解决?
关于上文的重排序问题,可以使用volatile关键字来解决。volatile一共有以下特性:
int number = 0;
volatile boolean flag = false;
public void method1() {
number = 6; // A
flag = true; // B
}
public void method2() {
if (flag) { // C
int i = number * 6; // D
}
}
由于volatile具有禁止代码重排序的特性,所以不会出现上文的B -> A的指令重排序。另外volatile具有可见性,falg的修改对线程2来说是可见的,线程会立刻感知到flag = ture
从而执行对i的赋值。以上问题可以通过volatile解决,和使用synchronized加锁是一样的效果。
另外大家注意一点,volatile的原子性指的是对volatile的读、写操作的原子性,但类似于volatile++
这种复合操作是没有原子性的。
面试官:那volatile可见性的原理是什么?
内存一共分为两种,线程的本地内存和线程外的主内存。对于一个volatile修饰的变量,任何线程对该变量的修改都会同步到主内存。而当读一个volatile修饰的变量时,JMM(Java Memory Model)会把该线程对应的本地内存置为无效,从而线程读取变量时读取的是主内存。
线程每次读操作都是读取主内存中最新的数据,所以volatile能够实现可见性的特性。
面试官:volatile有什么缺点吗?
企业生产上还是比较少用到volatile的,对于加锁操作会使用的更多些。
volatile++
这种复合操作,volatile不能确保原子性。我是南哥,南就南在Get到你的点赞点赞点赞。
创作不易,不妨点赞、收藏、关注支持一下,各位的支持就是我创作的最大动力❤️
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。