三歪:淦,已经熬到了第7面了
三歪:上次竟然先问了CAS,没有问synchronized锁,失策了失策了
三歪:但是,又多给了点时间我准备synchronized
三歪:嘿嘿,今天很稳,一点都不慌
三歪:干就完事了
三歪:面试官你好,请问面试可以开始了吗?
面试官:嗯,开始了。
面试官:今天我们来聊聊synchronized吧?
三歪:嗯嗯嗯,没问题
三歪:synchronized是一种互斥锁,一次只能允许一个线程进入被锁住的代码块
三歪:synchronized是Java的一个关键字,它能够将代码块/方法锁起来
三歪:如果synchronized修饰的是实例方法,对应的锁则是对象实例
三歪:如果synchronized修饰的是静态方法,对应的锁则是当前类的Class实例
三歪:如果synchronized修饰的是代码块,对应的锁则是传入synchronized的对象实例
面试官:嗯,要不你来讲讲synchronized的原理呗?
三歪:通过反编译可以发现
三歪:当修饰方法时,编译器会生成 ACC_SYNCHRONIZED 关键字用来标识
三歪:当修饰代码块时,会依赖monitorenter和monitorexit指令
三歪:但前面已经说了,无论synchronized修饰的是方法还是代码块,对应的锁都是一个实例(对象)
三歪:在内存中,对象一般由三部分组成,分别是对象头、对象实际数据和对齐填充
三歪:重点在于对象头,对象头又由几部分组成,但我们重点关注对象头Mark Word的信息就好了
三歪:Mark Word会记录对象关于锁的信息
三歪:又因为每个对象都会有一个与之对应的monitor对象,monitor对象中存储着当前持有锁的线程以及等待锁的线程队列
三歪:了解Mark Word和monitor对象是理解 synchronized 原理的前提
面试官:嗯,听说synchronized锁在 JDK 1.6 之后做了很多的优化,这块你了解多少呢?
三歪:其实是这样的,在JDK 1.6之前是重量级锁,线程进入同步代码块/方法 时
三歪:monitor对象就会把当前进入线程的Id进行存储,设置Mark Word的monitor对象地址,并把阻塞的线程存储到monitor的等待线程队列中
三歪:它加锁是依赖底层操作系统的 mutex 相关指令实现,所以会有用户态和内核态之间的切换,性能损耗十分明显
三歪:而JDK1.6 以后引入偏向锁和轻量级锁在JVM层面实现加锁的逻辑,不依赖底层操作系统,就没有切换的消耗
三歪:所以,Mark Word对锁的状态记录一共有4种:无锁、偏向锁、轻量级锁和重量级锁
面试官:简单来说说偏向锁、轻量级锁和重量级锁吧
三歪:嗯,没问题
三歪:偏向锁指的就是JVM会认为只有某个线程才会执行同步代码(没有竞争的环境)
三歪:所以在Mark Word会直接记录线程ID,只要线程来执行代码了,会比对线程ID是否相等,相等则当前线程能直接获取得到锁,执行同步代码
三歪:如果不相等,则用CAS来尝试修改当前的线程ID,如果CAS修改成功,那还是能获取得到锁,执行同步代码
三歪:如果CAS失败了,说明有竞争环境,此时会对偏向锁撤销,升级为轻量级锁。
三歪:在轻量级锁状态下,当前线程会在栈帧下创建Lock Record,LockRecord 会把Mark Word的信息拷贝进去,且有个Owner指针指向加锁的对象
三歪:线程执行到同步代码时,则用CAS试图将Mark Word的指向到线程栈帧的Lock Record,假设CAS修改成功,则获取得到轻量级锁
三歪:假设修改失败,则自旋(重试),自旋一定次数后,则升级为重量级锁
三歪:简单总结一下
三歪:synchronized锁原来只有重量级锁,依赖操作系统的mutex指令,需要用户态和内核态切换,性能损耗十分明显
三歪:重量级锁用到monitor对象,而偏向锁则在Mark Word记录线程ID进行比对,轻量级锁则是拷贝Mark Word到Lock Record,用CAS+自旋的方式获取。
三歪:引入了偏向锁和轻量级锁,就是为了在不同的使用场景使用不同的锁,进而提高效率。锁只有升级,没有降级
三歪:1)只有一个线程进入临界区,偏向锁
三歪:2)多个线程交替进入临界区,轻量级锁
三歪:3)多线程同时进入临界区,重量级锁
面试官:OK,明白了。看得出来你的基础还是可以的,lock锁你了解吗?
三歪:嗯,了解的。是…
面试官:过两天吧,后面再来细聊好了。