前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >对线面试官 - Synchronize Volatile | 通俗易懂的白话文讲解其原理实现

对线面试官 - Synchronize Volatile | 通俗易懂的白话文讲解其原理实现

作者头像
@派大星
发布2023-08-10 13:41:29
1390
发布2023-08-10 13:41:29
举报
文章被收录于专栏:码上遇见你

面试官:Synchronized有哪些特性?

派大星:Synchronized既保证了原子性也保证了可见性可重入(自己不停地加锁)

面试官:为什么synchronized可以保证共享变量的可见性?

派大星:在Java内存模型中,synchronized规定,线程在加锁时,先清空工作内存,在主内存中拷贝最新变量的副本到工作内存。执行完代码,将更改后的共享变量的值刷新到主内存中,释放互斥锁

面试官:为什么Synchronized是支持可重入的。

派大星:它必须要支持可重入锁,首先假设有一个父类 Synchronized m()方法。子类重写了方法**m()**方法也是Synchronized的并在子类中**supper.m()**了。如果Synchronized不支持可重入,那么直接就死锁了。

面试官:锁在什么情况下产生乱入的情况?这个有遇到过吗?

派大星:首先在加锁成功的时候,如果所内的代码块出现了异常,则会导致锁的一个释放,从而会产生锁乱入的一个情况。想要避免这种情况需要在锁定的代码块中进行捕获异常手动处理。让其继续执行。

面试官:Synchronized底层是如何实现的了解吗?

派大星:首先来说它锁定的是某个对象。在Java虚拟机里面并没有规定具体的视线,在HotSpot(Oracle 虚拟机的实现)里面,一个对象的头上面(64位)。头上面其中的两位(mark work)是标识着是否锁定了例如011.5后期(1.6)之前是重量级的后来进行了锁升级的概念。具体流程如下:

  • 偏向锁:markword 记录锁id
  • 自旋锁(CAS):默认10次,争抢锁。这里是在用户态(占用CPU),不需经过内核态(操作系统)
  • 重量级锁:去操作系统申请资源

但是锁只可以升级,不可以降级。

扩展阅读:

Synchronized的底层实现原理:Synchronized关键字在底层编译后的JVM指令中,会有monitoretermonitorexit两个指令。加锁会执行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里面执行的代码是等值的:例如:

代码语言:javascript
复制
public void m() {
    sychronized(this) {
        // todo 
    }
}
代码语言:javascript
复制
public sychronized void m() {
        // todo 
}

static方法是没有this对象的,如果Synchronized锁定的是static方法,那么等同于锁定的是synchronized(类对象)

面试官:针对业务的写加锁,读要不要加锁?

派大星:首先要考虑实际的业务场景,因为读不加锁的话可能会产生脏读的情况,当然如果业务场景并不会依据该读取结果做一些写的操作。那就是没有问题的。

面试官:在使用Synchronized使用的过程中需要注意什么?

派大星:不能使用String 常量IntegerLong等基础数据类型,容易和其它写的代码或类库使用一个常量,这样会导致如果是一个线程访问则会重入,非一个线程则会死锁。

派大星:锁定对象o,如果对象o的属性发生改变,不影响锁的使用,但是如果o变成另外一个对象,则锁定的对象发生改变。应该避免锁定对象的引用变成另外的对象,使用final

面试官:聊一聊Volatile?

派大星:了解JVM内存的都知道,堆内存是所有线程共享的内存,但是每个线程都有自己专属的区域(工作内存),如果在共享内存有一个值f=true的话,t1和t2两个线程同时访问的话。此时这个f就会被copy一份到对应线程的工作内存上,无论哪个线程对f执行操作都是现在自己的工作内存去发生改变,然后刷回共享内存,但时另外一个线程也无法确定什么时间从共享内存将改变后的值刷回自己的工作内存。也就是线程之间的不可见性这个时候就需要添加volatile关键字。它主要是保证线程可见性,禁止指令重排序。它底层是通过CPU的缓存一致性协议来保证的MESI。至于禁止指令重排序就是现在的CPU为了提高效率可能会并发的执行指令、或者将指令重新排序。最经典的案例就是DCL单例(懒汉式)是需要加Volatile关键字的。

派大星:Volatile(不能保证原子性)

code如下:

代码语言:javascript
复制
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到一半拥有了默认值,但不是初始值这里就会出现问题。

面试官:非常不错。希望你能考虑下我们的公司

派大星:感谢您的评价,我一定会认真考虑的

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-08-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码上遇见你 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档