Android并发编程高级面试题汇总最全最细面试题讲解持续更新中👊👊 👀你想要的面试题这里都有👀 👇👇👇
Synchronized的原理以及与ReentrantLock的区别。(360)
这道题想考察什么?
- 是否了解并发相关的理论知识
- 是否对于锁机制有个全面的理论认知
- 是否对于AQS原理有自己的理解
考察的知识点
- 锁的分类(公平锁、重入锁、重力度锁等等)
- ReentrantLock实现方式与Synchronized实现方式的异同点
考生应该如何回答
Synchronized的原理见**《4.8 Synchronized在JDK1.6之后做了哪些优化》**。
ReentrantLock与Synchronized的区别,除了一个是Java类实现,一个是关键字之外,还包括:
除此之外,ReenTrantLock相对于Synchronized还拥有自己的独有特性:
- ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
- ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
- ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。
volatile关键字干了什么?(什么叫指令重排) (字节跳动)
详细讲解
享学课堂移动互联网系统课程:架构师筑基必备技能《线程与进程的理论知识入门2》与《深入理解并发编程-AQS与JMM》
这道题想考察什么?
是否了解volatile关键字与真实场景使用
考察的知识点
volatile关键字的概念在项目中使用与基本知识
考生应该如何回答
volatile是java提供的可以声明在成员属性前的一个关键字。在声明中包含此关键字的作用有:
保证内存可见性
可见性是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到。
当对非volatile变量进行读写的时候,每个线程先从主内存拷贝变量到CPU缓存中,如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的CPU cache中。
volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,保证了每次读写变量都从主内存中读,跳过CPU cache这一步。当一个线程修改了这个变量的值,新值对于其他线程是立即得知的。
禁止指令重排
指令重排序是JVM为了优化指令、提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。指令重排序包括编译器重排序和运行时重排序。
latile变量禁止指令重排序。针对volatile修饰的变量,在读写操作指令前后会插入内存屏障,指令重排序时不能把后面的指令重排序到内存屏
示例说明:
示例说明:
double r = 2.1; //(1)
double pi = 3.14;//(2)
double area = pi*r*r;//(3)
虽然代码语句的定义顺序为1->2->3,但是计算顺序1->2->3与2->1->3对结果并无影响,所以编译时和运行时可以根据需要对1、2语句进行重排序。
指令重排带来的问题
如果一个操作不是原子的,就会给JVM留下重排的机会。
线程A中
{
context = loadContext();
inited = true;
}
线程B中
{
if (inited)
fun(context);
}
如果线程A中的指令发生了重排序,那么B中很可能就会拿到一个尚未初始化或尚未初始化完成的context,从而引发程序错误。
禁止指令重排的原理
volatile关键字提供内存屏障的方式来防止指令被重排,编译器在生成字节码文件时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
JVM内存屏障插入策略:
在每个volatile写操作的前面插入一个StoreStore屏障;
在每个volatile写操作的后面插入一个StoreLoad屏障;
在每个volatile读操作的后面插入一个LoadLoad屏障;
在每个volatile读操作的后面插入一个LoadStore屏障。
指令重排在双重锁定单例模式中的影响 基于双重检验的单例模式(懒汉型)
public class Singleton3 {
private static Singleton3 instance = null;
private Singleton3() {}
public static Singleton3 getInstance() {
if (instance == null) {
synchronized(Singleton3.class) {
if (instance == null)
instance = new Singleton3();// 非原子操作
}
}
return instance;
}
}
instance= new Singleton()并不是一个原子操作,其实际上可以抽象为下面几条JVM指令:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
上面操作2依赖于操作1,但是操作3并不依赖于操作2。所以JVM是可以针对它们进行指令的优化重排序的,经过重排序后如下:
memory =allocate(); //1:分配对象的内存空间
instance =memory; //3:instance指向刚分配的内存地址,此时对象还未初始化
ctorInstance(memory); //2:初始化对象
指令重排之后,instance指向分配好的内存放在了前面,而这段内存的初始化被排在了后面。在线程A执行这段赋值语句,在初始化分配对象之前就已经将其赋值给instance引用,恰好另一个线程进入方法判断instance引用不为null,然后就将其返回使用,导致出错。
volatile解决重排
用volatile关键字修饰instance变量,使得instance在读、写操作前后都会插入内存屏障,避免重排序。
public class Singleton3 {
private static volatile Singleton3 instance = null;
private Singleton3() {}
public static Singleton3 getInstance() {
if (instance == null) {
synchronized(Singleton3.class) {
if (instance == null)
instance = new Singleton3();
}
}
return instance;
}
}