Java并发编程艺术 - synchronized使用和原理

synchronized属于重量级锁,实现代码同步。

Java SE1.6优化引入了偏向锁和轻量级锁,同时支持锁升级,以减少获取锁和释放锁的性能消耗。

锁的对象

synchronized可以修饰Java非空对象,常见3种形式:

  • 锁当前实例
    修饰普通方法,例如:

    1
    2
    synchronized void add(){
    }

    那么该对象实例将会在执行该方法时阻塞,以保持同步执行,不可并行执行;但是不同对象可以并行执行。

  • 锁类所以对象
    修饰静态方法、全局变量、类,例如:

    1
    2
    synchronized static void add(){
    }
    1
    2
    synchronized (Singleton.class){
    }

    该类所有操作及类实例均会在执行时阻塞,以保持同步执行,不同对象也不可并行执行。

  • 锁方法块

    修饰实例或变量

    1
    2
    synchronized (this) {
    }
    1
    2
    synchronized (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 阻塞,响应慢 适合同步块执行慢,追求吞吐量
------ 本文结束------

本文标题:Java并发编程艺术 - synchronized使用和原理

文章作者:Perkins

发布时间:2019年05月05日

原始链接:https://perkins4j2.github.io/posts/43888/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。