修饰代码块
// 关键字在代码块上,锁为括号里面的对象
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 标志。
Monitor
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对象头
Java对象有对象头,实例数据,填充数据三部分组成。其中对象头由标记字段,类型指针,数组长度三部分组成。
偏向锁
偏向锁主要是用来优化同一个线程多次申请同一个锁的竞争。偏向锁的作用时当一个线程再次访问同步代码或方法时,只需在对象头上判断线程的偏向锁的线程ID是否为当前线程。如果是的话,则不用再次进入Monitor去竞争对象了。
如果有其他线程竞争该资源时,则改偏向锁就会被撤消。偏向锁的撤销需要等待全局安全点,暂停持有该锁的线程。同时检查该线程是否还在执行该方法,如果是,则升级锁,反之,则其他线程抢占。
因此,在高并发场景下,当大量线程同时竞争同一个锁资源时,偏向锁就会被撤销,发生 stop the word 后, 开启偏向锁无疑会带来更大的性能开销,这时我们可以通过添加 JVM 参数关闭偏向锁来调优系统性能,示例代码如下:
偏向锁设置方法
-XX:-UseBiasedLocking //关闭偏向锁(默认打开)
-XX:+UseHeavyMonitors //设置重量级锁
复制代码
轻量级锁
当另外有一个线程获取锁时,发现该锁已经是偏向锁了,那么就会通过CAS的方式去获取锁,如果获取成功,那么直接替换标记字段的类型线程ID为当前线程。如果获取失败,那么就会撤偏向锁,转为轻量级锁。
轻量级锁适用于线程交替执行同步块的场景,绝大部分的锁在整个同步周期内都不存在长时间的竞争。
自旋锁和重量级锁
轻量级锁CAS获取锁失败, 默认会通过自旋的方式来获取锁。自旋锁重试之后如果抢锁依然失败,同步锁就会升级至重量级锁,锁标志位改为 10。在这个状态下,未抢到锁的线程都会进入 Monitor,之后会被阻塞在 _WaitSet 队列中。
锁消除与锁粗化
JIT编译器在动态编译同步代码块的时候,会通过逃逸分析的技术。如果确定这个代码块只会被一个线程访问,那么就会进行锁消除。
锁粗化同理,就是在 JIT 编译器动态编译时,如果发现几个相邻的同步块使用的是同一个锁实例,那么 JIT 编译器将会把这几个同步块合并为一个大的同步块,从而避免一个线程“反复申请、释放同一个锁“所带来的性能开销。
减小锁粒度
这个主要是在代码层面进行优化。
例如,JDK8之前的ConcurrentHashMap,通过分段的机制来控制。
总结
- 检测Mark Word里面是不是当前线程ID,如果是,表示当前线程处于偏向锁
- 如果不是,则使用CAS将当前线程ID替换到Mark Word,如果成功则表示当前线程获得偏向锁,设置偏向标志位1
- 如果失败,则说明发生了竞争,撤销偏向锁,升级为轻量级锁
- 当前线程使用CAS将对象头的mark Word锁标记位替换为锁记录指针,如果成功,当前线程获得锁
- 如果失败,表示其他线程竞争锁,当前线程尝试通过自旋获取锁 for(;;)
- 如果自旋成功则依然处于轻量级状态
- 如果自旋失败,升级为重量级锁