全网最硬核的 synchronized 面试题深度解析

synchronized (this) {}

5)作用于静态成员变量,锁住的是该静态成员变量对象,由于是静态变量,因此全局只有一个。

public static Object monitor = new Object();

synchronized (monitor) {}

有些同学可能会搞混,但是其实很容易记,记住以下两点:

1)必须有“对象”来充当“锁”的角色。

2)对于同一个类来说,通常只有两种对象来充当锁:实例对象、Class 对象(一个类全局只有一份)。

Class 对象:静态相关的都是属于 Class 对象,还有一种直接指定 Lock.class。

实例对象:非静态相关的都是属于实例对象。

3、为什么调用 Object 的 wait/notify/notifyAll 方法,需要加 synchronized 锁?

这个问题说难也难,说简单也简单。说简单是因为,大家应该都记得有道题目:“sleep 和 wait 的区别”,答案中非常重要的一项是:“wait会释放对象锁,sleep不会”,既然要释放锁,那必然要先获取锁。

说难是因为如果没有联想到这个题目并且没有了解的底层原理,可能就完全没头绪了。

究其原因,因为这3个方法都会操作锁对象,所以需要先获取锁对象,而加 synchronized 锁可以让我们获取到锁对象。

来看一个例子:

public class SynchronizedTest {

private static final Object lock = new Object();

public static void testWait() throws InterruptedException {

lock.wait();

}

public static void testNotify() throws InterruptedException {

lock.notify();

}

}

在这个例子中,wait 会释放 lock 锁对象,notify/notifyAll 会唤醒其他正在等待获取 lock 锁对象的线程来抢占 lock 锁对象。

既然你想要操作 lock 锁对象,那必然你就得先获取 lock 锁对象。就像你想把苹果让给其他同学,那你必须先拿到苹果。

再来看一个反例:

public class SynchronizedTest {

private static final Object lock = new Object();

public static synchronized void getLock() throws InterruptedException {

lock.wait();

}

}

该方法运行后会抛出 IllegalMonitorStateException,为什么了,我们明明加了 synchronized 来获取锁对象了?

因为在 getLock 静态方法中加 synchronized 方法获取到的是 SynchronizedTest.class 的锁对象,而我们的 wait() 方法是要释放 lock 的锁对象。

这就相当于你想让给其他同学一个苹果(lock),但是你只有一个梨子(SynchronizedTest.class)。

4、synchronize 底层维护了几个列表存放被阻塞的线程?

这题是紧接着上一题的,很明显面试官想看看我是不是真的对 synchronize 底层原理有所了解。

synchronized 底层对应的 JVM 模型为 objectMonitor,使用了3个双向链表来存放被阻塞的线程:_cxq(Contention queue)、_EntryList(EntryList)、_WaitSet(WaitSet)。

当线程获取锁失败进入阻塞后,首先会被加入到 _cxq 链表,_cxq 链表的节点会在某个时刻被进一步转移到 _EntryList 链表。

具体转移的时刻?见题目30。

当持有锁的线程释放锁后,_EntryList 链表头结点的线程会被唤醒,该线程称为 successor(假定继承者),然后该线程会尝试抢占锁。

当我们调用 wait() 时,线程会被放入 _WaitSet,直到调用了 notify()/notifyAll() 后,线程才被重新放入 _cxq 或 _EntryList,默认放入 _cxq 链表头部。

objectMonitor 的整体流程如下图:

5、为什么释放锁时被唤醒的线程会称为“假定继承者”?被唤醒的线程一定能获取到锁吗?

因为被唤醒的线程并不是就一定获取到锁了,该线程仍然需要去竞争锁,而且可能会失败,所以该线程并不是就一定会成为锁的“继承者”,而只是有机会成为,所以我们称它为假定的。

这也是 synchronized 为什么是非公平锁的一个原因。

6、synchronized 是公平锁还是非公平锁?

非公平锁。

7、synchronized 为什么是非公平锁?非公平体现在哪些地方?

synchronized 的非公平其实在源码中应该有不少地方,因为设计者就没按公平锁来设计,核心有以下几个点:

1)当持有锁的线程释放锁时,该线程会执行以下两个重要操作:

  1. 先将锁的持有者 owner 属性赋值为 null

  2. 唤醒等待链表中的一个线程(假定继承者)。

在1和2之间,如果有其他线程刚好在尝试获取锁(例如自旋),则可以马上获取到锁。

2)当线程尝试获取锁失败,进入阻塞时,放入链表的顺序,和最终被唤醒的顺序是不一致的,也就是说你先进入链表,不代表你就会先被唤醒。

8、既然加了 synchronized 锁,那当某个线程调用了 wait 的时候明明还在 synchronized 块里,其他线程怎么进入到 synchronized 里去执行 notify 的?

如下例子:调用 lock.wait() 时,线程就阻塞在这边了,此时代码执行应该还在 synchronized 块里,其他线程为什么就能进入 synchronized 块去执行 notify() ?

public class SynchronizedTest {

private static final Object lock = new Object();

public static void testWait() throws InterruptedException {

synchronized (lock) {

// 阻塞住,被唤醒之前不会输出aa,也就是还没离开synchronized

lock.wait();

System.out.println(“aa”);

}

}

public static void testNotify() throws InterruptedException {

synchronized (lock) {

lock.notify();

System.out.println(“bb”);

}

}

}

只看代码确实会给人题目中的这种错觉,这也是 Object 的 wait() 和 notify() 方法很多人用不好的原因,包括我也是用的不太好。

这个题需要从底层去看,当线程进入 synchronized 时,需要获取 lock 锁,但是在调用 lock.wait() 的时候,此时虽然线程还在 synchronized 块里,但是其实已经释放掉了 lock 锁。

所以,其他线程此时可以获取 lock 锁进入到 synchronized 块,从而去执行 lock.notify()。

9、如果有多个线程都进入 wait 状态,那某个线程调用 notify 唤醒线程时是否按照进入 wait 的顺序去唤醒?

答案是否定的。上面在介绍 synchronized 为什么是非公平锁时也介绍了不会按照顺序去唤醒。

调用 wait 时,节点进入_WaitSet 链表的尾部。

调用 notify 时,根据不同的策略,节点可能被移动到 cxq 头部、cxq 尾部、EntryList 头部、EntryList 尾部等多种情况。

所以,唤醒的顺序并不一定是进入 wait 时的顺序。

10、notifyAll 是怎么实现全唤起的?

nofity 是获取 WaitSet 的头结点,执行唤起操作。

nofityAll 的流程,可以简单的理解为就是循环遍历 WaitSet 的所有节点,对每个节点执行 notify 操作。

11、JVM 做了哪些锁优化?

偏向锁、轻量级锁、自旋锁、自适应自旋、锁消除、锁粗化。

12、为什么要引入偏向锁和轻量级锁?为什么重量级锁开销大?

重量级锁底层依赖于系统的同步函数来实现,在 linux 中使用 pthread_mutex_t(互斥锁)来实现。

这些底层的同步函数操作会涉及到:操作系统用户态和内核态的切换、进程的上下文切换,而这些操作都是比较耗时的,因此重量级锁操作的开销比较大。

而在很多情况下,可能获取锁时只有一个线程,或者是多个线程交替获取锁,在这种情况下,使用重量级锁就不划算了,因此引入了偏向锁和轻量级锁来降低没有并发竞争时的锁开销。

13、偏向锁有撤销、膨胀,性能损耗这么大为什么要用呢?

偏向锁的好处是在只有一个线程获取锁的情况下,只需要通过一次 CAS 操作修改 markword ,之后每次进行简单的判断即可,避免了轻量级锁每次获取释放锁时的 CAS 操作。

如果确定同步代码块会被多个线程访问或者竞争较大,可以通过 -XX:-UseBiasedLocking 参数关闭偏向锁。

14、偏向锁、轻量级锁、重量级锁分别对应了什么使用场景?

1)偏向锁

适用于只有一个线程获取锁。当第二个线程尝试获取锁时,即使此时第一个线程已经释放了锁,此时还是会升级为轻量级锁。

但是有一种特例,如果出现偏向锁的重偏向,则此时第二个线程可以尝试获取偏向锁。

2)轻量级锁

适用于多个线程交替获取锁。跟偏向锁的区别在于可以有多个线程来获取锁,但是必须没有竞争,如果有则会升级会重量级锁。有同学可能会说,不是有自旋,请继续往下看。

3)重量级锁

适用于多个线程同时获取锁。

15、自旋发生在哪个阶段?

自旋发生在重量级锁阶段。

网上99.99%的说法,自旋都是发生在轻量级锁阶段,但是实际看了源码(JDK8)之后,并不是这样的。

轻量级锁阶段并没有自旋操作,在轻量级锁阶段,只要发生竞争,就是直接膨胀成重量级锁。

而在重量级锁阶段,如果获取锁失败,则会尝试自旋去获取锁。

16、为什么要设计自旋操作?

因为重量级锁的挂起开销太大。

一般来说,同步代码块内的代码应该很快就执行结束,这时候竞争锁的线程自旋一段时间是很容易拿到锁的,这样就可以节省了重量级锁挂起的开销。

17、自适应自旋是如何体现自适应的?

自适应自旋锁有自旋次数限制,范围在:1000~5000。

如果当次自旋获取锁成功,则会奖励自旋次数100次,如果当次自旋获取锁失败,则会惩罚扣掉次数200次。

所以如果自旋一直成功,则JVM认为自旋的成功率很高,值得多自旋几次,因此增加了自旋的尝试次数。

相反的,如果自旋一直失败,则JVM认为自旋只是在浪费时间,则尽量减少自旋。

18、synchronized 锁能降级吗?

答案是可以的。

具体的触发时机:在全局安全点(safepoint)中,执行清理任务的时候会触发尝试降级锁。

当锁降级时,主要进行了以下操作:

1)恢复锁对象的 markword 对象头;

2)重置 ObjectMonitor,然后将该 ObjectMonitor 放入全局空闲列表,等待后续使用。

19、synchronized 和 ReentrantLock 的区别

1)底层实现:synchronized 是 Java 中的关键字,是 JVM 层面的锁;ReentrantLock 是 JDK 层次的锁实现。

2)是否需要手动释放:synchronized 不需要手动获取锁和释放锁,在发生异常时,会自动释放锁,因此不会导致死锁现象发生;ReentrantLock 在发生异常时,如果没有主动通过 unLock() 去释放锁,很可能会造成死锁现象,因此使用 ReentrantLock 时需要在 finally 块中释放锁。

3)锁的公平性:synchronized 是非公平锁;ReentrantLock 默认是非公平锁,但是可以通过参数选择公平锁。

4)是否可中断:synchronized 是不可被中断的;ReentrantLock 则可以被中断。

5)灵活性:使用 synchronized 时,等待的线程会一直等待下去,直到获取到锁;ReentrantLock 的使用更加灵活,有立即返回是否成功的,有响应中断、有超时时间等。

6)性能上:随着近些年 synchronized 的不断优化,ReentrantLock 和 synchronized 在性能上已经没有很明显的差距了,所以性能不应该成为我们选择两者的主要原因。官方推荐尽量使用 synchronized,除非 synchronized 无法满足需求时,则可以使用 Lock。

20、synchronized 锁升级流程?

核心流程如下图所示,请保存后放大查看,有一些概念看不懂很正常,会在文章后面陆续介绍,请继续往下看。

如果画质模糊可以去我分享面试题的百度云盘里下载原图。

synchronized 加锁流程图

PS:从此处开始,往下的内容偏底层原理,都是对锁升级流程图的详细解析。大部分面试官可能不会直接问到,但是在聊到锁升级的时候,以下的内容都可以说。

21、synchronized 的底层实现

synchronized 的底层实现主要区分:方法和代码块,如下图例子。

/**

  • @author joonwhee

  • @date 2019/7/6

*/

public class SynchronizedDemo {

private static final Object lock = new Object();

public static void main(String[] args) {

// 锁作用于代码块

synchronized (lock) {

System.out.println(“hello word”);

}

}

// 锁作用于方法

public synchronized void test() {

System.out.println(“test”);

}

}

将该代码进行编译后,查看其字节码,核心代码如下:

{

public com.joonwhee.SynchronizedDemo();

descriptor: ()V

flags: ACC_PUBLIC

Code:

stack=1, locals=1, args_size=1

0: aload_0

1: invokespecial #1 // Method java/lang/Object.“”😦)V

4: return

LineNumberTable:

line 9: 0

public static void main(java.lang.String[]);

descriptor: ([Ljava/lang/String;)V

flags: ACC_PUBLIC, ACC_STATIC

Code:

stack=2, locals=3, args_size=1

0: getstatic #2 // Field lock:Ljava/lang/Object;

3: dup

4: astore_1

5: monitorenter // 进入同步块

6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;

9: ldc #4 // String hello word

11: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

14: aload_1

15: monitorexit // 退出同步块

16: goto 24

19: astore_2

20: aload_1

21: monitorexit // 退出同步块

22: aload_2

23: athrow

24: return

Exception table:

from to target type

6 16 19 any

19 22 19 any

public synchronized void test();

descriptor: ()V

flags: ACC_PUBLIC, ACC_SYNCHRONIZED // ACC_SYNCHRONIZED 标记

Code:

stack=2, locals=1, args_size=1

0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;

3: ldc #6 // String test

5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

8: return

LineNumberTable:

line 20: 0

line 21: 8

}

synchronized 修饰代码块时,编译后会生成  monitorenter 和 monitorexit 指令,分别对应进入同步块和退出同步块。可以看到有两个 monitorexit,这是因为编译时 JVM 为代码块添加了隐式的 try-finally,在 finally 中进行了锁释放,这也是为什么 synchronized 不需要手动释放锁的原因。

synchronized 修饰方法时,编译后会生成 ACC_SYNCHRONIZED 标记,当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了则会先尝试获得锁。

两种实现其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

22、介绍下 Mark Word?

在介绍 Mark Word 之前,需要先了解对象的内存布局。 HotSpot 中,对象在堆内存中的存储布局可以分为三部分:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。

1)对象头(Header)

主要包含两类信息:Mark Word 和 类型指针。

Mark Word 记录了对象的运行时数据,例如:HashCode、GC分代年龄、偏向标记、锁标记、偏向的线程ID、偏向纪元(epoch)等,32 位的 markword 如下图所示。

类型指针,指向它的类型元数据的指针,Java 虚拟机通过这个指针来确定该对象时哪个类的实例。如果对象是数组,则需要有一个用于记录数组长度的数据。

2)实例数据(Instance Data)

对象存储的真正有效信息,即我们在代码里定义的各种类型的字段内容。

3)对齐填充(Padding)

Hotspot 要求对象的大小必须是8字节的整数倍,因此,如果实例数据不是8字节的整数倍时,需要通过该字段进行填充。

23**、介绍下 Lock Record?**

锁记录,这个大家应该都听过,用于轻量级锁时暂存对象的 markword。

Lock Record 在源码中为 BasicObjectLock,源码如下:

class BasicObjectLock VALUE_OBJ_CLASS_SPEC {

private:

BasicLock _lock;

oop _obj;

};

class BasicLock VALUE_OBJ_CLASS_SPEC {

private:

volatile markOop _displaced_header;

};

其实就两个属性:

1)_displaced_header:用于轻量级锁中暂存锁对象的 markword,也称为 displaced mark word。

2)_obj:指向锁对象。

Lock Record 除了用于暂存 markword 之外,还有一个重要的功能是用于实现锁重入的计数器,当每次锁重入时,会用一个 Lock Record 来记录,但是此时 _displaced_header 为 null。

这样在解锁的时候,每解锁1次,就移除1个 Lock Record。移除时,判断 _displaced_header 是否为 null。如果是,则代表是锁重入,则不会执行真正的解锁;否则,代表这是最后一个 Lock Record,此时会真正执行解锁操作。

24、什么匿名偏向?

所谓的匿名偏向是指该锁从未被获取过,也就是第一次偏向,此时的特点是锁对象 markword 的线程 ID 为0。

最后

关于面试刷题也是有方法可言的,建议最好是按照专题来进行,然后由基础到高级,由浅入深来,效果会更好。当然,这些内容我也全部整理在一份pdf文档内,分成了以下几大专题:

  • Java基础部分

  • 算法与编程

  • 数据库部分

  • 流行的框架与新技术(Spring+SpringCloud+SpringCloudAlibaba)

这份面试文档当然不止这些内容,实际上像JVM、设计模式、ZK、MQ、数据结构等其他部分的面试内容均有涉及,因为文章篇幅,就不全部在这里阐述了。

作为一名程序员,阶段性的学习是必不可少的,而且需要保持一定的持续性,这次在这个阶段内,我对一些重点的知识点进行了系统的复习,一方面巩固了自己的基础,另一方面也提升了自己的知识广度和深度。

,会用一个 Lock Record 来记录,但是此时 _displaced_header 为 null。

这样在解锁的时候,每解锁1次,就移除1个 Lock Record。移除时,判断 _displaced_header 是否为 null。如果是,则代表是锁重入,则不会执行真正的解锁;否则,代表这是最后一个 Lock Record,此时会真正执行解锁操作。

24、什么匿名偏向?

所谓的匿名偏向是指该锁从未被获取过,也就是第一次偏向,此时的特点是锁对象 markword 的线程 ID 为0。

最后

关于面试刷题也是有方法可言的,建议最好是按照专题来进行,然后由基础到高级,由浅入深来,效果会更好。当然,这些内容我也全部整理在一份pdf文档内,分成了以下几大专题:

  • Java基础部分

[外链图片转存中…(img-8qi329Yn-1714579785004)]

  • 算法与编程

[外链图片转存中…(img-gBvye2w8-1714579785004)]

  • 数据库部分

[外链图片转存中…(img-668Op7Cd-1714579785005)]

  • 流行的框架与新技术(Spring+SpringCloud+SpringCloudAlibaba)

[外链图片转存中…(img-Llb126RM-1714579785005)]

这份面试文档当然不止这些内容,实际上像JVM、设计模式、ZK、MQ、数据结构等其他部分的面试内容均有涉及,因为文章篇幅,就不全部在这里阐述了。

作为一名程序员,阶段性的学习是必不可少的,而且需要保持一定的持续性,这次在这个阶段内,我对一些重点的知识点进行了系统的复习,一方面巩固了自己的基础,另一方面也提升了自己的知识广度和深度。

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 27
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值