synchronized属于重量级锁,实现代码同步。
Java SE1.6优化引入了偏向锁和轻量级锁,同时支持锁升级,以减少获取锁和释放锁的性能消耗。
锁的对象
synchronized可以修饰Java非空对象,常见3种形式:
锁当前实例
修饰普通方法,例如:1
2synchronized void add(){
}那么该对象实例将会在执行该方法时阻塞,以保持同步执行,不可并行执行;但是不同对象可以并行执行。
锁类所以对象
修饰静态方法、全局变量、类,例如:1
2synchronized static void add(){
}1
2synchronized (Singleton.class){
}该类所有操作及类实例均会在执行时阻塞,以保持同步执行,不同对象也不可并行执行。
锁方法块
修饰实例或变量
1
2synchronized (this) {
}1
2synchronized (var) {
}该类所有操作及类实例均会在执行该代码块时阻塞,以保持同步执行,不同对象也不可并行执行;但对其他代码块不产生影响。
锁的实现
synchronized由JVM实现,通过在代码块前后添加monitorenter和monitorexit指令。
线程执行到monitorenter尝试获取对象对应的monitor,即对象锁;如持有monitor锁,则对象处于锁定状态。
Java对象头
synchronized锁信息存储在对象头中。
对象头包含Mark Word,存储对象HashCode、分代年龄和锁状态。
锁状态包括轻量锁、重量锁、偏向锁和GC标记。
锁升级
锁的4种状态,由低到高:
- 无锁
- 偏向锁
- 轻量锁
- 重量锁
锁因为竞争可以由低到高升级,不可由高到低降级,以提高锁获取和释放效率。
偏向锁
大多数情况,锁是同一线程多次获取,因此引入偏向锁。
在线程获取锁后,在对象头和栈帧中记录线程ID;后续该线程进入和退出代码同步块时不需要CAS获取和释放锁。
偏向锁释放
当出现线程竞争偏向锁时,持有偏向锁的线程需要释放偏向锁。
释放偏向锁时,需要等待安全点,即该线程没有正在执行的字节码。
然后暂停拥有偏向锁的线程,若该线程处于不活动状态,将对象头设置为无锁状态;若活着,则重新偏向其他线程。
偏向锁优化
偏向锁在程序启动后会延迟激活,-XX:BiasedLockingStartupDelay=0
可以关闭延迟。
如果程序通常处于竞争状态,可以通过-XX:-UseBiasedLocking=false
关闭偏向锁,那么程序默认会进入轻量级锁状态。
轻量锁
轻量锁加锁
JVM在执行同步代码块前,先在线程栈帧创建锁记录空间,并将对象头Mark Word复制到锁记录空间。
线程通过CAS将对象头的Mark Word替换为锁记录空间地址;若成功,则获取锁成功。
如失败,则存在锁竞争,线程通过自旋来获取锁。
轻量锁解锁
在解锁时,线程采用CAS将锁记录空间信息替换回对象头的Mark Word;若成功,则释放锁。
若失败,则说明存在锁竞争,则升级为重量级锁。
重量锁
因为自旋需要消耗CPU,所以轻量锁升级到重量锁后不能降级。
处于重量级锁状态,其他线程获取锁将被阻塞,指导当前线程释放锁并唤醒其他线程竞争。
锁比较
锁 | 优点 | 缺点 | 说明 |
---|---|---|---|
偏向锁 | 速度快,和非同步方法执行效率差不多 | 出现锁竞争时,需要额外释放锁 | 适合一个线程并发访问同步块 |
轻量锁 | 无阻塞,响应速度快 | 始终得不到锁的线程会自旋消耗CPU | 适合方法块执行块,追求响应速度 |
重量级锁 | 无自旋,不消耗CPU | 阻塞,响应慢 | 适合同步块执行慢,追求吞吐量 |