// 关键字在代码块上,锁为括号里面的对象 public void method2() { Object o = new Object(); synchronized (o) { // code } } 复制代码
Synchronized 在修饰同步代码块时,是由 monitorenter 和 monitorexit 指令来实现同步的。进入 monitorenter 指令后,线程将持有 Monitor 对象,退出 monitorenter 指令后,线程将释放该 Monitor 对象。
// access flags 0x1 public method2()V TRYCATCHBLOCK L0 L1 L2 null TRYCATCHBLOCK L2 L3 L2 null L4 LINENUMBER 16 L4 NEW java/lang/Object DUP INVOKESPECIAL java/lang/Object.<init> ()V ASTORE 1 L5 LINENUMBER 17 L5 ALOAD 1 DUP ASTORE 2 MONITORENTER L0 LINENUMBER 19 L0 ALOAD 2 MONITOREXIT L1 GOTO L6 L2 FRAME FULL [com/dragon/learn/leean1/SynchronizedTest java/lang/Object java/lang/Object] [java/lang/Throwable] ASTORE 3 ALOAD 2 MONITOREXIT L3 ALOAD 3 ATHROW L6 LINENUMBER 20 L6 FRAME CHOP 1 RETURN L7 LOCALVARIABLE this Lcom/dragon/learn/leean1/SynchronizedTest; L4 L7 0 LOCALVARIABLE o Ljava/lang/Object; L5 L7 1 MAXSTACK = 2 MAXLOCALS = 4 } 复制代码
当 Synchronized 修饰同步方法时,并没有发现 monitorenter 和 monitorexit 指令,而是出现了一个 ACC_SYNCHRONIZED 标志。
JVM 中的同步是基于进入和退出管程(Monitor)对象实现的。每个对象实例都会有一个 Monitor,Monitor 可以和对象一起创建、销毁。Monitor 是由 ObjectMonitor 实现,而 ObjectMonitor 是由 C++ 的 ObjectMonitor.hpp 文件实现,如下所示:
ObjectMonitor() { _header = NULL; _count = 0; // 记录个数 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; // 处于 wait 状态的线程,会被加入到 _WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; // 处于等待锁 block 状态的线程,会被加入到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; } 复制代码
当多个线程同时访问同一个代码块时,首先会将这先线程放入ContenionList和EntryList中。之后线程通过操作系统的Mutex Lock来获取锁。如果获取到了,则执行相应的代码。如果没有获取到,则重新进入ContenionList。如果调用了wait方法,则会进入WaitSet。当其他线程调用notify方法时会唤醒并重新进入EntryList.
Java对象有对象头,实例数据,填充数据三部分组成。其中对象头由标记字段,类型指针,数组长度三部分组成。
偏向锁主要是用来优化同一个线程多次申请同一个锁的竞争。偏向锁的作用时当一个线程再次访问同步代码或方法时,只需在对象头上判断线程的偏向锁的线程ID是否为当前线程。如果是的话,则不用再次进入Monitor去竞争对象了。
如果有其他线程竞争该资源时,则改偏向锁就会被撤消。偏向锁的撤销需要等待全局安全点,暂停持有该锁的线程。同时检查该线程是否还在执行该方法,如果是,则升级锁,反之,则其他线程抢占。
因此,在高并发场景下,当大量线程同时竞争同一个锁资源时,偏向锁就会被撤销,发生 stop the word 后, 开启偏向锁无疑会带来更大的性能开销,这时我们可以通过添加 JVM 参数关闭偏向锁来调优系统性能,示例代码如下:
-XX:-UseBiasedLocking //关闭偏向锁(默认打开) -XX:+UseHeavyMonitors //设置重量级锁 复制代码
当另外有一个线程获取锁时,发现该锁已经是偏向锁了,那么就会通过CAS的方式去获取锁,如果获取成功,那么直接替换标记字段的类型线程ID为当前线程。如果获取失败,那么就会撤偏向锁,转为轻量级锁。
轻量级锁适用于线程交替执行同步块的场景,绝大部分的锁在整个同步周期内都不存在长时间的竞争。
轻量级锁CAS获取锁失败, 默认会通过自旋的方式来获取锁。自旋锁重试之后如果抢锁依然失败,同步锁就会升级至重量级锁,锁标志位改为 10。在这个状态下,未抢到锁的线程都会进入 Monitor,之后会被阻塞在 _WaitSet 队列中。
JIT编译器在动态编译同步代码块的时候,会通过逃逸分析的技术。如果确定这个代码块只会被一个线程访问,那么就会进行锁消除。
锁粗化同理,就是在 JIT 编译器动态编译时,如果发现几个相邻的同步块使用的是同一个锁实例,那么 JIT 编译器将会把这几个同步块合并为一个大的同步块,从而避免一个线程“反复申请、释放同一个锁“所带来的性能开销。