一、应用总结
synchronized 同步块是 Java 提供的一种原子性内置锁,Java 中的每个对象都可以把它当作 一个同步锁来使用,这些 Java 内置的使用者看不到的锁被称为内置锁,也叫作监视器锁。
二、字节码解析
synchronized是JVM内置锁,基于Monitor机制实现,依赖底层操作系统的互斥原语 Mutex(互斥量),它是一个重量级锁,性能较低。当然,JVM内置锁在1.5之后版本做了重大的 优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、自适应自旋(Adaptive Spinning)等技术来减少锁操 作的开销,内置锁的并发性能已经基本与Lock持平。
同步方法 是通过方法上的 access_flags中设置ACC_SYNCHRONIZED标志来实现;
同步代码块是通过monitorenter和monitorexit来实现。两个指令的执行是JVM通过调用操作系统的互斥 原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态 之间来回切换,从而导致性能损耗,所以说synchronized是一个重量级锁。
三、Monitor(管程/监视器)
monitor 在操作系统层面的专业术语叫“管程”,管程指的是管理共享变量和对共享变量的操作过程,让它们支持并发;
3.1、MESA模型
mesa是属于解决管程相关的一种模型,也是被广泛使用最多的一种;
3.2 monitor的主要数据结构
hotspot ObjectMonitor.hpp 文件中
//ObjectMonitor.hpp
// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
ObjectMonitor() {
_header = NULL; //对象头
_count = 0;
_waiters = 0,
_recursions = 0; //锁的重入次数
_object = NULL;
_owner = NULL; //标识拥有该monitor的线程(当前获取锁的线程)
_WaitSet = NULL; //等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
FreeNext = NULL ;
_EntryList = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失 败的线程)
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
四、对象布局
Mark Word是如何记录锁状态的
Hotspot通过markOop类型实现Mark Word,具体实现位于markOop.hpp文件中
// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ------------------------------------------>| (CMS free block)
// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)
//
// unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
// JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
// narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
// unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
//
enum { locked_value = 0, //00轻量级所
unlocked_value = 1, // 001 无锁
monitor_value = 2, //10 重量级锁
marked_value = 3, //11 GC标记
biased_lock_pattern = 5 //101 偏向锁
};
// Constants
enum { age_bits = 4,
lock_bits = 2,
biased_lock_bits = 1,
max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits,
cms_bits = LP64_ONLY(1) NOT_LP64(0),
epoch_bits = 2
};
五、JOL工具验证
5.1 包依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.11</version>
</dependency>
5.2 场景验证
- 偏向锁
偏向锁模式存在偏向锁延迟机制:HotSpot 虚拟机在启动后有个 4s 延迟才会对每个新创建的对象开启偏向锁模式。JVM启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等操作。这个过程中会使用大量synchronized关键字对对象加锁,且这些锁大多数都不是偏向锁。 为了减少初始化时间,JVM默认延时加载偏向锁。
‐XX:BiasedLockingStartupDelay=0 //设置偏向锁启动延迟,默认值是4000ms
‐XX:‐UseBiasedLocking //禁止偏向锁
‐XX:+UseBiasedLocking //开启偏向锁
代码验证
public class BiasedLockDemo {
private static final Logger log= LoggerFactory.getLogger(BiasedLockDemo.class);
public static void main(String[] args) throws InterruptedException {
log.debug("创建的对象========"+ClassLayout.parseInstance(new Object()).toPrintable());
Thread.sleep(4000);
log.debug("4秒后创建的对象======="+ClassLayout.parseInstance(new Object()).toPrintable());
}
}
public class BiasedLockDemo {
private static final Logger log= LoggerFactory.getLogger(BiasedLockDemo.class);
public static void main(String[] args) throws InterruptedException {
/*log.debug("创建的对象========"+ClassLayout.parseInstance(new Object()).toPrintable());
Thread.sleep(4000);
log.debug("5秒后创建的对象======="+ClassLayout.parseInstance(new Object()).toPrintable());
*/
Thread.sleep(4000);
Object obj = new Object();
new Thread(()->{
log.debug(Thread.currentThread().getName()+" 开始执行 "+ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
log.debug(Thread.currentThread().getName()+" 获取锁执行 "+ClassLayout.parseInstance(obj).toPrintable());
}
log.debug(Thread.currentThread().getName()+" 释放锁 "+ClassLayout.parseInstance(obj).toPrintable());
},"thread-1").start();
Thread.sleep(4000);
log.debug("最终======= "+ClassLayout.parseInstance(obj).toPrintable());
}
}
偏向锁撤销之调用对象HashCode
调用锁对象的obj.hashCode()或System.identityHashCode(obj)方法会导致该对象的偏向锁被撤销。因为对于一个对象,其HashCode只会生成一次并保存,偏向锁是没有地方保存 hashcode的。
轻量级锁会在锁记录中记录 hashCode
重量级锁会在 Monitor 中记录 hashCode
当对象处于可偏向(也就是线程ID为0)和已偏向的状态下,调用HashCode计算将会使对象再也 无法偏向:
当对象可偏向时,MarkWord将变成未锁定状态,并只能升级成轻量锁;
当对象正处于偏向锁时,调用HashCode将使偏向锁强制升级成重量锁。
public class BiasedLockDemo {
private static final Logger log= LoggerFactory.getLogger(BiasedLockDemo.class);
public static void main(String[] args) throws InterruptedException {
Thread.sleep(4000);
Object obj = new Object();
log.debug(Thread.currentThread().getName()+" 没用调用HashCode== "+ClassLayout.parseInstance(obj).toPrintable());
obj.hashCode();
new Thread(()->{
log.debug(Thread.currentThread().getName()+" 开始执行 "+ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
log.debug(Thread.currentThread().getName()+" 获取锁执行 "+ClassLayout.parseInstance(obj).toPrintable());
}
log.debug(Thread.currentThread().getName()+" 释放锁 "+ClassLayout.parseInstance(obj).toPrintable());
},"thread-1").start();
Thread.sleep(4000);
log.debug("最终======= "+ClassLayout.parseInstance(obj).toPrintable());
}
}
Thread.sleep(4000);
Object obj = new Object();
new Thread(()->{
log.debug(Thread.currentThread().getName()+" 开始执行 "+ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
log.debug(Thread.currentThread().getName()+" 加锁成功 开始执行 "+ClassLayout.parseInstance(obj).toPrintable());
obj.hashCode();
log.debug(Thread.currentThread().getName()+" 加锁成功 调用HashCode "+ClassLayout.parseInstance(obj).toPrintable());
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(Thread.currentThread().getName()+" 释放锁 "+ClassLayout.parseInstance(obj).toPrintable());
},"thread-1").start();
偏向锁撤销之调用wait/notify
偏向锁状态执行obj.notify() 会升级为轻量级锁,调用obj.wait(timeout) 会升级为重量级锁
Thread.sleep(4000);
Object obj = new Object();
new Thread(()->{
log.debug(Thread.currentThread().getName()+" 开始执行 "+ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
log.debug(Thread.currentThread().getName()+" 加锁成功 开始执行 "+ClassLayout.parseInstance(obj).toPrintable());
obj.notify();
log.debug(Thread.currentThread().getName()+" 加锁成功 调用notify "+ClassLayout.parseInstance(obj).toPrintable());
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(Thread.currentThread().getName()+" 释放锁 "+ClassLayout.parseInstance(obj).toPrintable());
},"thread-1").start();
Thread.sleep(4000);
Object obj = new Object();
new Thread(()->{
log.debug(Thread.currentThread().getName()+" 开始执行 "+ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
log.debug(Thread.currentThread().getName()+" 加锁成功 开始执行 "+ClassLayout.parseInstance(obj).toPrintable());
try {
obj.wait(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(Thread.currentThread().getName()+" 加锁成功 调用wait "+ClassLayout.parseInstance(obj).toPrintable());
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(Thread.currentThread().getName()+" 释放锁 "+ClassLayout.parseInstance(obj).toPrintable());
},"thread-1").start();
- 轻量级锁
如果偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段,此时Mark Word 的结构也变为轻量级锁结构。轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间多个线程访问同一把锁的场合,就会导致轻量级锁膨胀为重量级锁。
偏向锁升级轻量级锁
public static void main(String[] args) throws InterruptedException {
Thread.sleep(4000);
Object obj = new Object();
Thread thread1 = new Thread(() -> {
log.debug(Thread.currentThread().getName() + " 开始执行 " + ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
log.debug(Thread.currentThread().getName() + " 加锁成功执行中 " + ClassLayout.parseInstance(obj).toPrintable());
}
log.debug(Thread.currentThread().getName() + " 释放锁 " + ClassLayout.parseInstance(obj).toPrintable());
}, "thread-1");
thread1.start();
//设置竞争不那么激励
Thread.sleep(200);
Thread thread2 = new Thread(() -> {
log.debug(Thread.currentThread().getName() + " 开始执行 " + ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
log.debug(Thread.currentThread().getName() + " 加锁成功执行中 " + ClassLayout.parseInstance(obj).toPrintable());
}
log.debug(Thread.currentThread().getName() + " 释放锁 " + ClassLayout.parseInstance(obj).toPrintable());
}, "thread-2");
thread2.start();
Thread.sleep(4000);
log.debug("最终======= "+ClassLayout.parseInstance(obj).toPrintable());
}
1.
六、锁升级总结
七、锁优化
- 偏向锁批量重偏向&批量撤销
从偏向锁的加锁解锁过程中可看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时,再将偏向锁撤销为无锁状态或升级为轻量级,会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降。于是,就有了批量重偏向与批量撤销的机制。
应用场景
批量重偏向(bulk rebias)机制是为了解决:一个线程创建了大量对象并执行了初始的同步操 作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。 批量撤销(bulk revoke)机制是为了解决:在明显多线程竞争剧烈的场景下使用偏向锁是不合适的
原理
以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销 操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问 题,因此会进行批量重偏向。 每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的Mark Word中也有该 字段,其初始值为创建该对象时class中的epoch的值。每次发生批量重偏向时,就将该值+1,同 时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新 值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其 他线程,也不会执行撤销操作,而是直接通过CAS操作将其Mark Word的Thread Id 改成当前线 程Id。当达到重偏向阈值(默认20)后,假设该class计数器继续增长,当其达到批量撤销的阈值后 (默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后, 对于该class的锁,直接走轻量级锁的逻辑。
JVM参数
intx BiasedLockingBulkRebiasThreshold = 20 //默认偏向锁批量重偏向阈值
intx BiasedLockingBulkRevokeThreshold = 40 //默认偏向锁批量撤销阈值
总结
1. 批量重偏向和批量撤销是针对类的优化,和对象无关。
2. 偏向锁重偏向一次之后不可再次重偏向。
3. 当某个类已触发批量撤销机制后,JVM会默认当前类产生了严重问题,剥夺该类的新实例对象使用偏向锁权利
-
自旋锁优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程 已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
自旋的目的是为了减少线程挂起的次数,尽量避免直接挂起线程(挂起操作涉及系统调 用,存在用户态和内核态切换,这才是重量级锁最大的开销) -
锁粗化
StringBuffer buffer = new StringBuffer()
buffer.append("aaa").append(" bbb").append(" ccc");
- 锁消除
使用逃逸分析,编译器可以对代码做如下优化:
1.同步省略或锁消除(Synchronization Elimination)。如果一个对象被发现只能从一个线程被访问 到,那么对于这个对象的操作可以不考虑同步。
2.将堆分配转化为栈分配(Stack Allocation)。如果一个对象在子程序中被分配,要使指向该对象 的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
3.分离对象或标量替换(Scalar Replacement)。有的对象可能不需要作为一个连续的内存结构存 在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。