前言

最近技术圈子里因log4j的漏洞炸开了锅。

Synchronized锁在面试当中难免会遇到,那么如何完美应对面试官角度刁钻的问题就显得尤为重要。阿巴阿巴以身作则,给大家贡献面试经验。

回家等通知

面试官: synchronized应该了解吧?讲讲。

阿巴阿巴: 嗯嗯,了解一些,synchronized是Java中的关键字,它的作用主要是用来同步,一般叫它同步锁,一般可以用在方法上,以及代码块上。

阿巴阿巴: 用在方法上好像锁的的对象,用在代码块上如果修饰的是对象则锁的是对象,如果修饰的是类,那么锁的是该类的所有对象。

面试官: 不错,那synchronized可以用在构造方法上吗?上锁的过程你了解吗?

阿巴阿巴: 嗯...这个...不太清楚。

面试官: 那可以讲一下锁的优化吗?

阿巴阿巴: 嗯? 锁还有优化吗?不是很清楚哦。

面试官: 好的,那今天先面到这里吧,你回去等我着通知哈😈

阿巴阿巴: 好的。

当场发offer

面试官: synchronized应该了解吧?讲讲。

阿巴阿巴: 嗯嗯,了解一些,synchronized是Java中的关键字,它的作用主要是用来同步,一般叫它同步锁,一般可以用在实例方法上、静态方法上以及代码块上,主要是维护一个状态,这个状态就是同一时刻,只能有一个线程去访问synchronized修饰的方法或代码块。

阿巴阿巴: 用在实例方法上锁的是掉用该方法的对象,用在静态方法上,锁的是当前类的所有对象,用在代码块上如果修饰的是对象则锁的是对象,如果修饰的是类,那么锁的是该类的所有对象。(画图强化记忆)

alt

    // synchronized用在静态方法上
    public synchronized static void test01() {
    }
    
    // synchronized用在实例方法上
    public synchronized void test02() {
    }
    
    // synchronized用来修饰对象
    public void test03() {
        synchronized (this) {}
    }
    
    // synchronized用来修饰当前类
    public void test04() {
        synchronized (TestSyn.class) {}
    }

面试官: 不错,那synchronized可以用在构造方法上吗?上锁的过程你了解吗?

阿巴阿巴: synchronized不能直接加在构造方法上,但是可以在构造方法里使用synchronized的代码块。

阿巴阿巴: 上锁过程这里涉及到JDK版本问题,在JDK1.5及之前的话,synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这俩个字节码指令,在执行monitorenter指令的时候对象锁(这个对象锁包括对象实例或Class对象),如果说获取的这个对象没有被锁定,或者说当前线程已经获取到该对象的锁了(synchronized是可重入锁,即已经获取到锁的线程可以再次获取锁,而不需要再进行同步),那么就把锁的计数器加1,

阿巴阿巴: 同样,如果在执行monitorexit这个指令时,就把锁的计数器减1,这样当计数器的值为0时,锁就被释放了。倘若线程获取对象锁没成功,那么就会一直阻塞等待直到锁被释放。

阿巴阿巴: synchronized重量级锁的实现是由C++代码实现的,其中有个ObjectMonitor队,下面展示下代码中的重要属性。

ObjectMonitor() {
    _recursions   = 0;     //重入次数
    _owner        = NULL;  //指向持有ObjectMonitor对象的线程 
    _WaitSet      = NULL;  //调用wait后,线程会被加入到_WaitSet,WaitSet是第一个节点
    _cxq          = NULL ; //多线程竞争锁进入时的单向链表
    _EntryList    = NULL ; //等待获取锁的线程,会被加入到该列表,_EntryList是第一个节点
}

阿巴阿巴: 下面是线程流动图。

alt

如上图所示

0 当多个线程同时竞争时,那么这些线程会被放入到EntryList队列,此时线程处于阻塞状态

1 当一个线程获取到了对象的monitor后,那么就可以进入运行状态,这时候ObjectMonitor对象的/_owner指向当前线程,_count加1表示当前对象锁被一个线程获取。

2 当运行状态的线程调用wait()方法,那么当前线程释放monitor对象,进入等待状态,ObjectMonitor对象的/_owner变为null,_count减1,同时线程进入_WaitSet队列。

3 直到有线程调用notify()方法唤醒该线程,则该线程进入_EntryList队列,竞争到锁再进入_Owner区。

4 如果当前线程执行完毕,那么也释放monitor对象,ObjectMonitor对象的/_owner变为null,_count减1。

阿巴阿巴: 而JDK 1.5版本之前的synchronized,每次加锁都需要从用户态(运行用户程序)切换到内核态(运行操作系统程序、操作硬件等),这种切换对系统资源的消耗是巨大的,因此JDK 1.6版本对synchronized进行了优化,引入了下面这些概念

自旋锁

自适应性自旋

锁消除

锁粗化

偏向锁

轻量级锁

重量锁

面试官: 愿闻其详

阿巴阿巴: 自旋锁的引入主要是因为大多数情况下,一个线程占用锁的时间不会持续很长时间,如果有其他线程竞争,直接将竞争失败的线程挂起再恢复,显然这种消耗是巨大的,所以采用一种“观望”的手段,即让该线程稍做等待,看看这段时间内占有锁的线程是否会释放锁,这就是自旋。

阿巴阿巴: 然而,自旋也没能彻底解决该问题,需要考虑到占有锁的线程对锁的占用,如果占用过久那么就会导致自旋锁一直做无用的自选操作,从而消耗CPU资源,因此设置一个自旋的次数阈值显得尤为重要,这个阈值也需要设置成合适的值,不会过高也不会过低。

阿巴阿巴: 自适应自旋锁的诞生。自适应的意思就是说自旋的次数或者时间不再固定了,而是由前一次在同一个锁上的自旋的次数或者时间来决定:如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,M某种意义上来说它将允许自旋等待持续相对更长的时间。相反的,如果自旋很少成功获得过,那在以后要获取这个锁时将可能减少自旋时间、或次数,从而来避免浪费CPU资源。

阿巴阿巴: 锁消除即在同步的代码中分析发现无论如何都不会出现锁的竞争,那么就可以将该锁进行消除,这个分析被称为逃逸分析,如果有一个段同步的代码不会被其他线程所访问到,那么这个同步也是无意义的。

阿巴阿巴: 锁的粗化指的是如果一段代码一直在不停的给一个对象进行加锁、解锁,比如在循环体中进行加锁、解锁操作,就算没有线程竞争,也会产生巨大消耗的,对于这种情况可以考虑将锁的范围扩大,这个过程就是粗化。

阿巴阿巴: 偏向锁在保证线程安全的情况下,其实不一定会有线程的竞争,也就是不一定会有互斥,如果一个锁对象没有其他没有其他线程竞争,那么JVM会默认其为偏向锁,偏向锁默认只有第一个申请锁的线程会使用锁且不会有其他线程来竞争锁,因此,只需要在Mark Word中CAS记录owner,如果记录更新成功,则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner就可以零成本的直接获得锁;如果这时候有其他线程竞争,那么偏向锁就会膨胀为轻量级锁。

阿巴阿巴: 使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;轻量锁适合于俩个线程交替运行,但是没有产生实质上得竞争,如果发生了锁竞争,接下来轻量锁将膨胀为重量级锁。

面试官: 讲的很好,不错,可以回去准备后面的面试了😈。

阿巴阿巴: 好的。

本期面试到此结束,下期阿巴阿巴被问到了更难的对象头和锁相关的东西,期待她完美的表现吧!

❤️/ 感谢支持 /

以上便是本次分享的全部内容,希望对你有所帮助^_^ ​ 喜欢的话别忘了 分享、点赞、收藏 三连哦~ ​ 欢迎关注公众号 程序员巴士,来自字节、虾皮、招银的三端兄弟,分享编程经验、技术干货与职业规划,助你少走弯路进大厂。 ​