1.JVM是如何处理和识别Synchronized的?
我们从字节码角度分析synchronized的实现:
-
Synchronized(锁对象){}同步代码块底层实现方式是monitorenter和monitorexit指令。
-
修饰普通同步方法时底层实现方式是执行指令会检查方法是否设置ACC_SYNCHRONIZED,如果设置了,则会先持有monitor锁(其实就是管程,锁对象),然后在执行方法,最后释放锁(无论方法执行完或出现异常)。
-
修饰静态同步方法时底层实现方式是执行指令会检查方法是否同时设置ACC_STATIC和ACC_SYNCHRONIZED,ACC_STATIC也用于分辨锁是类锁还是对象锁。
2.为什么任何一个类的对象都可以成为锁对象?
在HotSpot虚拟机中,监视器monitor采用的是ObjectMonitor实现的,在Java中,Object是每个类的父类,所以每个对象天生都带着一个对象监视器。在ObjectMonitor.java源代码中我们发现里面调用了objectMonitor.cpp文件,在objectMonitor.cpp里面又调用了ObjectMonitor.hpp,而在hpp文件中很明确的记录了正在持有此锁的线程、锁的重入次数等数据。
3.Synchronized锁的升级
Synchronized锁的状态主要依赖对象头中的MarkWord中锁标志位和偏向锁位。
3.1下面我们就表述锁升级的过程(重点)
初始状态下,一个对象被实例化后,如果还没有任何线程使用此锁,那么它就为无锁状态(偏向锁位为0,锁标志位为01),当线程A第一次占用此锁时,MarkWord中会记录线程A的线程id,然后升级为偏向锁(偏向锁位为1,锁标志位为01),然后下一个线程访问时,会看MarkWord中记录的线程id是否和访问的线程一致,如果一致,就相当于还是线程A一直在访问,那么就会自动的获取锁,无需每次CAS去更新对象头,但是如果发现线程的id不一致,那么就发生了竞争,比如线程B来访问了,发现MarkWord中记录的线程id和自己的不一致,那么就会尝试使用CAS来替换MarkWord里面的线程id为自己线程B的线程id,如果修改竞争成功了,那么ok,MarkWord里面的线程idA更换为线程B的id,锁不会升级,还是偏向锁,但是如果线程B修改竞争失败,那么锁的状态就需要发生改变了,首先就是要先撤销偏向锁,先等待全局的安全点(STW),同时检查正持有偏向锁的线程A执行到哪里了,如果说线程A正在处于同步代码块中,相当于线程A还没有执行完,那么会将锁升级为轻量锁(偏向锁位为0,锁标志位为00),线程A继续执行同步代码块,而正在竞争的线程B会自动进行自旋。但如果说线程A刚好执行完同步代码块,此时会设置为无锁的状态,线程A,B会同时开始竞争。如下图:
假如此时锁升级为了轻量级锁,JVM会在每个线程的栈帧中创建用于存储锁记录的空间(Displaced Mark Word),若此时线程A想要获取轻量级锁,会把锁对象的MarkWord拷贝复制到自己的DMW里面,然后线程A再尝试利用CAS将锁对象中的MarkWord中的指向记录改为指向线程A栈中的Lock Record的指针,此时如果线程A的CAS失败了,就说明线程B正在占用此锁,线程A就会通过不断自旋来获取锁,等到线程B执行完后,线程B还要将轻量级锁释放,线程B使用CAS操作将DMW的内容重新复制回锁对象的Mark Word里面。如果此时有大量的线程涌入,参与竞争,一个线程自旋到一定的次数,锁就会会升级为重量级锁(偏向锁位为0,锁标志位为10),没拿到锁的线程会等待操作系统的调动,就不在主动的去抢占获取锁了。具体这个自旋次数在Java8之后是自适应自旋锁。
-
线程如果自旋成功了,那下次自旋的最大次数会增加,因为JVM认为既然上次成功了,那么这一次也大概率会成功
-
如果很少会自选成功,那么下次会减少自旋的次数甚至不自旋,避免CPU空转。
3.2几个需要说明的小问题?
1.JDK15废除了偏向锁
JDK15以后逐步废弃偏向锁,需要手动开启------->因为维护成本高。
2.MarkWord中指向记录在不同状态的指向不同
-
偏向锁:MarkWord存储的是偏向的线程ID
-
轻量锁:MarkWord存储的是指向线程栈中Lock Record的指针
-
重量锁:MarkWord存储的是指向堆中锁的Monitor(监视器)对象,修改里面的owner来实现。
3.无锁会默认到偏向锁
实际上无锁是默认会自动升级为偏向锁的,但是启动时间有延迟,可以通过添加参数,让其在程序启动时立即启动。
4.锁升级后,hashcode去哪里了?
我们可以发现,hashcode值的位置和锁指向的内存位置会冲突,那么内部是怎么解决的呢——>
-
在无锁的状态下,对象的hashcode()值存储在Mark Word中,此时它就再也无法进入到偏向锁状态了。
-
如果已经在偏向锁状态下,才调用hashcode()方法,偏向锁的状态会被立即取消,锁会膨胀为重量级锁。
-
在轻量级锁状态下,会在DMW中保存拷贝的Mark Word的值,释放锁后,会将这些信息重新写回到对象头的Mark Word中(相当于覆盖了)。
-
ObjectMonitor类里面有字段会记录非加锁状态下的Mark Word,锁释放后也会重新写回到对象头中的Mark Word中。
3.3JIT编译器对锁的优化
JIT对锁的优化分为锁消除和锁粗化,其实这两个概念挺乏味的。
3.3.1锁消除
简单来说就是,如果每个线程都拥有一把锁,那么我们写的加锁代码就毫无意义了,从JIT角度来看就是无视它了,消除了对锁的使用。示例代码:
public class LockClearUpDemo {static Object object = new Object();public void m1() {//锁消除问题,JIT会无视它,synchronized(o)每次new出来的,加锁就无意义了Object o = new Object();synchronized (o) {System.out.println("-----------hello LockClearUpDemo" + "\t" + o.hashCode() + "\t" + object.hashCode());}}public static void main(String[] args) {LockClearUpDemo lockClearUpDemo = new LockClearUpDemo();for (int i = 0; i < 10; i++) {new Thread(() -> {lockClearUpDemo.m1();}, String.valueOf(i)).start();}}
}
3.3.2锁粗化
如方法中多个同步块首尾相接,前后使用的都是同一个锁对象,那么JIT编译器会把这几个synchronized块合并为一个大块,加粗锁的范围。
public class LockBigDemo {static Object objectLock = new Object();public static void main(String[] args) {new Thread(() -> {synchronized (objectLock) {System.out.println("111111111111");}synchronized (objectLock) {System.out.println("222222222222");}synchronized (objectLock) {System.out.println("333333333333");}synchronized (objectLock) {System.out.println("444444444444");}//底层JIT的锁粗化优化synchronized (objectLock) {System.out.println("111111111111");System.out.println("222222222222");System.out.println("333333333333");System.out.println("444444444444");}}, "t1").start();}
}
4.Synchronized的具体实现
线程代码进入到Synchronized代码块时会自动获取锁对象,这时其他线程访问时会被阻塞,直到Synchroinzed代码块执行完毕或抛出异常,调用wait()方法都会释放锁对象。在进入Synchronized代码块时会将主内存的变量读取到自己的工作内存,在退出的时候会把工作内存的更新值写入到主内存。Java中Synchronized通过在锁对象的对象头设置标记,达到获取锁和释放锁的目的。