面试官:Synchronized有哪些特性?
派大星:Synchronized既保证了原子性
也保证了可见性
、可重入(自己不停地加锁)
面试官:为什么synchronized可以保证共享变量的可见性?
派大星:在Java内存模型中,synchronized规定,线程在加锁时,先清空工作内存,在主内存中拷贝最新变量的副本到工作内存。执行完代码,将更改后的共享变量的值刷新到主内存中,释放互斥锁
面试官:为什么Synchronized是支持可重入的。
派大星:它必须要支持可重入锁,首先假设有一个父类 Synchronized m()方法。子类重写了方法**m()**
方法也是Synchronized的并在子类中**supper.m()**
了。如果Synchronized不支持可重入,那么直接就死锁了。
面试官:锁在什么情况下产生乱入的情况?这个有遇到过吗?
派大星:首先在加锁成功的时候,如果所内的代码块出现了异常,则会导致锁的一个释放,从而会产生锁乱入的一个情况。想要避免这种情况需要在锁定的代码块中进行捕获异常手动处理。让其继续执行。
面试官:Synchronized底层是如何实现的了解吗?
派大星:首先来说它锁定的是某个对象。在Java虚拟机里面并没有规定具体的视线,在HotSpot(Oracle 虚拟机的实现)里面,一个对象的头上面(64位)。头上面其中的两位(mark work
)是标识着是否锁定了例如01
1.5后期(1.6)之前是重量级的后来进行了锁升级的概念。具体流程如下:
但是锁只可以升级,不可以降级。
扩展阅读:
Synchronized的底层实现原理:Synchronized关键字在底层编译后的JVM指令中,会有
monitoreter
和monitorexit
两个指令。加锁会执行monitoreter
指令,解锁会执行monitorexit
指令。每个对象都有一个monitor
,比如一个对象实例就有一个monitor
,一个类的Class对象也有一个monitor,如果要对这个对象加锁,那么必须获取这个对象关联的monitor的lock锁。加锁的过程原理大致是这样的:monitor里面有一个计数器,从0开始。如果一个线程要获取monitor的锁,就要看它的计数器是不是0,如果为0则说明没人获取锁,它可以获取锁,并对计数器+1。Synchronized是可重入锁
,具体的表现形式为:假设线程T1第一次Synchronized那里已经获取到了对象O的monitor的锁,计数器+1,然后第二次Synchronized那里会再次获取对象O的monitor的锁。此时计数器会再次+1变成2。Synchronized是互斥锁
。其表现形式为:假设线程T1在第一次Synchronized那里,发现当前对对象O的monitor锁的计数器是大于0的。就意味着别人加锁了。此时线程T1就会进入到block阻塞状态。
面试官:什么情况下用自旋锁好,什么时候系统锁比较好?
派大星:加锁代码执行时间长的用系统锁,特别短线程少适合自旋锁。
Synchronized 方法 和Synchronized this里面执行的代码是等值的:例如:
public void m() {
sychronized(this) {
// todo
}
}
public sychronized void m() {
// todo
}
static
方法是没有this对象的,如果Synchronized锁定的是static方法,那么等同于锁定的是synchronized(类对象)
面试官:针对业务的写加锁,读要不要加锁?
派大星:首先要考虑实际的业务场景,因为读不加锁的话可能会产生脏读
的情况,当然如果业务场景并不会依据该读取结果做一些写的操作。那就是没有问题的。
面试官:在使用Synchronized使用的过程中需要注意什么?
派大星:不能使用String 常量
、Integer
、Long
等基础数据类型,容易和其它写的代码或类库使用一个常量,这样会导致如果是一个线程访问则会重入,非一个线程则会死锁。
派大星:锁定对象o,如果对象o的属性发生改变,不影响锁的使用,但是如果o变成另外一个对象,则锁定的对象发生改变。应该避免锁定对象的引用变成另外的对象,使用final
面试官:聊一聊Volatile?
派大星:了解JVM内存的都知道,堆内存是所有线程共享的内存,但是每个线程都有自己专属的区域(工作内存),如果在共享内存有一个值f=true的话,t1和t2两个线程同时访问的话。此时这个f就会被copy一份到对应线程的工作内存上,无论哪个线程对f执行操作都是现在自己的工作内存去发生改变,然后刷回共享内存,但时另外一个线程也无法确定什么时间从共享内存将改变后的值刷回自己的工作内存。也就是线程之间的不可见性这个时候就需要添加volatile关键字。它主要是保证线程可见性,禁止指令重排序。它底层是通过CPU的缓存一致性协议来保证的MESI。至于禁止指令重排序就是现在的CPU为了提高效率可能会并发的执行指令、或者将指令重新排序。最经典的案例就是DCL单例(懒汉式)是需要加Volatile关键字的。
派大星:Volatile(不能保证原子性)
code如下:
public class SingleDCL{
private static SingleDCL INSTANCE;
private SingleDCL(){
}
public synchronized static SingleDCL getInstance() {
if(INSTANCE == null) {
try {
//
} catch (InterruptedExecutption e) {
e.printStackTrance();
}
INSTANCE = new SingleDCL();
}
return INSTANCE;
}
}
// 双重检查的懒汉式单例也不会有问题
public class SingleDCL{
private static volatile SingleDCL INSTANCE;
private SingleDCL(){
}
public static SingleDCL getInstance() {
if(INSTANCE == null) {
synchronized (SingleDCL.class) {
if(INSTANCE == null) {
try {
//
} catch (InterruptedExecutption e) {
e.printStackTrance();
}
INSTANCE = new SingleDCL();
}
}
}
return INSTANCE;
}
}
面试官:为什么DCL单例需要加volatile关键字?
派大星:结论:由于指令重排序 关于这个问题我们要先知道new 对象的一个过程:1.申请内存空间(默认值int 0 ) 2.初始化成员变量(初始值) 3.最后一步是赋值(指向变量地址) 第3步和第2步互换了位置。对象new到一半拥有了默认值,但不是初始值这里就会出现问题。
面试官:非常不错。希望你能考虑下我们的公司
派大星:感谢您的评价,我一定会认真考虑的