天天看点

Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念

Java3yのsynchronized

一、 Monitor 概念

1、 Java 对象头

(重点)

对象头

对象头包含两部分:

运行时元数据(Mark Word)

类型指针 (Klass Word)

  1. 运行时元数据

    • 哈希值(HashCode)

      ,可以看作是堆中对象的地址
    • GC分代年龄(年龄计数器)

      (用于新生代from/to区晋升老年代的标准, 阈值为15)
    • 锁状态标志 (用于JDK1.6对synchronized的优化 -> 轻量级锁)
    • 线程持有的锁
    • 偏向线程ID (用于JDK1.6对synchronized的优化 -> 偏向锁)
    • 偏向时间戳
  2. 类型指针

    • 指向

      类元数据InstanceKlass,确定该对象所属的类型

      。指向的其实是方法区中存放的类元信息

说明:如果对象是数组,还需要记录数组的长度

  • 以 32 位虚拟机为例,普通对象的对象头结构如下,其中的

    Klass Word

    类型指针

    ,指向

    方法区

    对应的

    Class对象

Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
  • 数组对象
    Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
  • 其中 Mark Word 结构为:

    无锁(001)、偏向锁(101)、轻量级锁(00)、重量级锁(10)

    Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
  • 所以一个对象的结构如下:
    Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念

2、 Monitor 原理 (Synchronized底层实现-重量级锁)

多线程同时访问临界区: 使用重量级锁
  • JDK6对Synchronized的优先状态:

    偏向锁–>轻量级锁–>重量级锁

  • Monitor

    被翻译为

    监视器

    或者

    管程

每个Java对象

都可以

关联一个(操作系统的)Monitor

,如果使用

synchronized

对象上锁(重量级)

,该

对象头的MarkWord

中就被设置为

指向Monitor对象

指针

下图原理解释:
  • Thread1

    访问到

    synchronized(obj)

    中的

    共享资源

    的时候
    • 首先会将synchronized中的

      锁对象

      对象头

      MarkWord

      去尝试指向

      操作系统

      Monitor

      对象. 让锁对象中的MarkWord和Monitor对象相关联. 如果关联成功, 将obj对象头中的

      MarkWord

      对象状态

      从01改为10。
    • 因为Monitor没有和其他的obj的MarkWord相关联, 所以

      Thread1

      就成为了该

      Monitor

      的Owner(所有者)。
    • 又来了个

      Thread1

      执行synchronized(obj)代码, 它首先会看看能不能执行该

      临界区

      的代码; 它会检查obj是否关联了Montior, 此时已经有关联了, 它就会去看看该Montior有没有所有者(Owner), 发现有所有者了(Thread2);

      Thread1

      也会和该Monitor关联, 该线程就会进入到它的

      EntryList(阻塞队列)

      ;
    • Thread2

      执行完

      临界区

      代码后, Monitor的

      Owner(所有者)

      就空出来了. 此时就会

      通知

      Monitor中的EntryList阻塞队列中的线程, 这些线程通过

      竞争

      , 成为新的

      所有者

      Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
  • 刚开始时

    Monitor

    中的

    Owner为null

  • 当Thread-2 执行synchronized(obj){}代码时就会将Monitor的所有者Owner 设置为 Thread-2,上锁成功,Monitor中同一时刻只能有一个Owner
  • 当Thread-2 占据锁时,如果线程Thread-3,Thread-4也来执行synchronized(obj){}代码,就会进入

    EntryList

    中变成

    BLOCKED状态

  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,

    竞争时是非公平的 (仍然是抢占式)

  • 图中 WaitSet 中的Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲wait-notify 时会分析

注意:
  • synchronized 必须是进入同一个锁对象的monitor 才有上述的效果; —> 也就要使用同一把锁
  • 不加 synchronized的锁对象不会关联监视器,不遵从以上规则
    Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
它加锁就是依赖底层操作系统的

mutex

相关指令实现, 所以会造成

用户态和内核态之间的切换

, 非常耗性能 !
  • 在JDK6的时候, 对synchronized进行了优化, 引入了

    轻量级锁, 偏向锁

    , 它们是在JVM的层面上进行加锁逻辑, 就没有了切换的消耗~

3、synchronized原理

代码如下

static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
    synchronized (lock) {
        counter++;
    }
}
           
  • 反编译后的部分字节码
    Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
注意:方法级别的 synchronized 不会在字节码指令中有所体现

4、synchronized 原理进阶

  • 小故事
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念

5、轻量级锁 (用于优化Monitor这类的重量级锁)

通过

锁记录

的方式, 场景 : 多个线程交替进入临界区
  • 轻量级锁的使用场景

    : 如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是没有人可以竞争的),那么可以使用

    轻量级锁来进行优化

  • 轻量级锁对使用者是透明的,即语法仍然是

    synchronized

    (jdk6对synchronized的优化),假设有两个方法同步块,利用同一个对象加锁
  • eg:

    线程A来操作临界区的资源,

    给资源加锁,到执行完临界区代码,释放锁

    的过程, 没有线程来竞争, 此时就可以使用

    轻量级锁

    ; 如果

    这期间

    有线程来竞争的话, 就会

    升级为重量级锁(synchronized)

static final Object obj = new Object();
public static void method1() {
     synchronized( obj ) {
         // 同步块 A
         method2();
     }
}
public static void method2() {
     synchronized( obj ) {
         // 同步块 B
     }
}
           
  • 每次指向到

    synchronized代码块

    时,都会在

    栈帧中

    创建

    锁记录(Lock Record)对象

    每个线程都会包括一个锁记录的结构

    ,锁记录内部可以储存

    对象的MarkWord

    锁对象引用reference

    Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
    Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
  • 锁记录

    中的

    Object reference指向锁对象地址

    ,并且尝试用

    CAS(compare and sweep)

    栈帧中的锁记录的(lock record 地址 00)

    替换

    Object对象的Mark Word

    ,将Mark Word 的值(01)存入锁记录(lock record地址)中 ------

    相互替换

    • 01 表示

      无锁

      (看Mark Word结构, 数字的含义)

    • 00表示

      轻量级锁

      Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念

重点:

  • 如果

    cas替换成功

    , 获得了轻量级锁,那么

    对象

    对象头储存的就是锁记录的地址和状态00

    ,如下所示
    • 线程中锁记录, 记录了锁对象的锁状态标志; 锁对象的对象头中存储了锁记录的地址和状态, 标志哪个线程获得了锁
    • 此时

      栈帧

      中存储了

      对象的对象头

      中的

      锁状态标志

      ,年龄计数器,哈希值等;

      对象的对象头中

      就存储了

      栈帧中锁记录的地址和状态00

      , 这样的话

      对象

      就知道了是

      哪个线程锁住自己

      Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
  • 如果

    cas替换失败,有两种情况

    : ① 锁膨胀 ② 重入锁失败
    • 1、如果是

      其它线程

      已经持有了

      该Object的轻量级锁

      ,那么表示有竞争,将进入

      锁膨胀阶段

      • 此时

        对象Object

        对象头中已经存储了别的线程的

        锁记录地址 00

        ,指向了其他线程;
    • 2、如果是

      自己的线程已经执行了synchronized进行加锁

      ,那么

      再添加一条 Lock Record 作为重入锁的计数

      – 线程多次加锁, 锁重入
      • 在上面代码中,

        临界区中

        又调用了

        method2

        , method2中又进行了一次

        synchronized加锁操作

        , 此时就会在

        虚拟机栈

        中再开辟一个method2方法对应的栈帧(栈顶), 该栈帧中又会存在一个

        独立

        Lock Record

        , 此时它发现

        对象的对象头中指向的就是自己线程中栈帧的锁记录

        ; 加锁也就失败了. 这种现象就叫做

        锁重入

        ; 线程中有多少个锁记录, 就能表明该线程对这个对象加了几次锁 (锁重入计数)
        Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念

轻量级锁解锁流程 :

  • 线程退出synchronized代码块

    的时候,如果获取的是

    取值为 null 的锁记录

    ,表示有

    锁重入

    ,这时重置锁记录,

    表示重入计数减一

    Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
  • 当线程退出synchronized代码块的时候,如果

    获取的锁记录取值不为 null

    ,那么使用cas将Mark Word的值恢复给对象, 将直接替换的内容还原。
    • 成功则解锁成功 (轻量级锁解锁成功)
    • 失败,表示有竞争, 则

      说明轻量级锁进行了锁膨胀

      已经升级为重量级锁

      ,进入重量级锁解锁流程 (Monitor流程)

6、锁膨胀

  • 如果在尝试

    加轻量级锁

    的过程中,

    cas替换操作无法成功

    ,这是有一种情况就是其它线程已经为这个对象加上了轻量级锁,这是就要进行

    锁膨胀(有竞争)

    将轻量级锁变成重量级锁。

  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁, 此时发生

    锁膨胀

    Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
  • 这时Thread-1加轻量级锁失败,进入锁膨胀流程
    • 因为

      Thread-1

      线程加轻量级锁失败, 轻量级锁没有阻塞队列的概念, 所以此时就要

      为对象申请Monitor锁(重量级锁)

      ,让

      Object指向重量级锁地址 10

      ,然后

      自己进入Monitor 的EntryList 变成BLOCKED状态

Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
  • Thread-0 线程

    执行完

    synchronized同步块

    时,使用cas将Mark Word的值恢复给对象头, 肯定恢复失败,因为对象的对象头中存储的是

    重量级锁的地址,状态变为10了

    之前的是00, 肯定恢复失败。那么会

    进入重量级锁的解锁过程

    ,即按照

    Monitor的地址找到Monitor对象,将Owner设置为null,唤醒EntryList中的Thread-1线程

7、自旋锁优化 (优化重量级锁竞争)

  • 当发生

    重量级锁竞争的时候

    ,还可以使用

    自旋来进行优化 (不加入Monitor的阻塞队列EntryList中)

    ,如果当前线程自旋成功(即在自旋的时候持锁的线程释放了锁),那么当前线程就可以不用进行上下文切换

    (持锁线程执行完synchronized同步块后,释放锁,Owner为空,唤醒阻塞队列来竞争,胜出的线程得到cpu执行权的过程)

    就获得了

  • 优化的点: 不用将

    线程

    加入到阻塞队列, 减少cpu切换.
  1. 自旋重试成功的情况

    Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
  2. 自旋重试失败的情况

    ,自旋了一定次数还是没有等到 持锁的线程释放锁, 线程2就会加入Monitor的阻塞队列(EntryList)
    Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
  • 自旋会

    占用 CPU 时间

    ,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
  • Java 6 之后自旋锁是自适应

    的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。Java 7 之后不能控制是否开启自旋功能

8、偏向锁 (biased lock) (用于优化轻量级锁重入)

场景: 没有竞争的时候, 一个线程中多次使用

synchronized

需要重入加锁的情况; (只有一个线程进入临界区)
  • 在经常需要竞争的情况下就不使用偏向锁, 因为偏向锁是默认开启的, 我们可以通过JVM的配置, 将偏向锁给关闭
  • 将进入临界区的线程的ID, 直接设置给锁对象的Mark word, 下次该线程又获取锁, 发现线程ID是自己, 就不需要CAS了
  • 轻量级的锁

    中,我们可以发现,如果同一个线程对同一个对象进行

    重入锁

    时,也需要执行CAS替换操作,这是有点耗时。
  • 那么java6开始引入了

    偏向锁

    ,将进入临界区的线程的ID, 直接设置给锁对象的Mark word, 下次该线程又获取锁, 发现线程ID是自己, 就不需要CAS了
    • 升级为轻量级锁的情况 (会进行偏向锁撤销) : 获取偏向锁的时候, 发现线程ID不是自己的, 此时通过CAS替换操作, 操作成功了, 此时该线程就获得了锁对象。(

      此时是交替访问临界区, 撤销偏向锁, 升级为轻量级锁

      )
    • 升级为重量级锁的情况 (会进行偏向锁撤销) : 获取偏向锁的时候, 发现线程ID不是自己的, 此时通过CAS替换操作, 操作失败了, 此时说明发生了锁竞争。(

      此时是多线程访问临界区, 撤销偏向锁, 升级为重量级锁

      )

例如:

public class Test {
    static final Object obj = new Object();

    public static void m1() {
        synchronized (obj) {
            // 同步块A
            m2();
        }
    }

    public static void m2() {
        synchronized (obj) {
            // 同步块B
            m3();
        }
    }

    public static void m3() {
        synchronized (obj) {
            // 同步块C
        }
    }
}
           
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
8.1、偏向锁状态 (了解)
  • 运行时元数据(Mark Word)

    的结构如下:
    Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念

-

Normal:一般状态,没有加任何锁

,前面62位保存的是对象的信息,最后2位为状态(01),倒数第三位表示是否使用偏向锁(未使用:0)

-

Biased:偏向状态,使用偏向锁

,前面54位保存的当前线程的ID,最后2位为状态(01),倒数第三位表示是否使用偏向锁(使用:1)

-

Lightweight:使用轻量级锁

,前62位保存的是锁记录的指针,最后2位为状态(00)

-

Heavyweight:使用重量级锁

,前62位保存的是Monitor的地址指针,最后2位为状态(10)

Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
  • 如果开启了偏向锁(默认开启),在创建对象时,对象的Mark Word后三位应该是101
  • 但是偏向锁默认是有延迟的,不会再程序一启动就生效,而是会在程序运行一段时间(几秒之后),才会对创建的对象设置为偏向状态
  • 如果没有开启偏向锁,对象的Mark Word后三位应该是001

一个对象的创建过程

- 如果开启了

偏向锁(默认是开启的)

,那么对象刚创建之后,Mark Word 最后三位的值101,并且这是它的

ThreadId

epoch

age(年龄计数器)

都是

,在加锁的时候进行设置这些的值.

-

偏向锁默认是延迟

的,不会在程序启动的时候立刻生效,如果想避免延迟,可以添加虚拟机参数来禁用延迟:

-XX:BiasedLockingStartupDelay=0

来禁用延迟

- 注意 :

处于偏向锁的对象解锁后,线程id仍存储于对象头中

Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念

输出结果:

Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
  • 测试

    禁用偏向锁

    :如果

    没有开启偏向锁

    ,那么对象创建后最后三位的值为

    001

    ,这时候它的hashcode,age都为0,hashcode是第一次用到

    hashcode

    时才赋值的。在上面测试代码运行时在添加 VM 参数

    -XX:-UseBiasedLocking

    禁用偏向锁

    (禁用偏向锁则优先使用轻量级锁)

    ,退出

    synchronized

    状态变回 001
    • 禁止偏向锁, 虚拟机参数

      -XX:-UseBiasedLocking

      ; 优先使用

      轻量级锁

    • 输出结果: 最开始状态为001,然后加轻量级锁变成00,最后恢复成001
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
8.2、撤销偏向锁-hashcode方法 (了解)
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
  • 测试

    hashCode

    :当

    调用对象的hashcode方法

    的时候就会

    撤销这个对象的偏向锁

    ,因为使用偏向锁时没有位置存

    hashcode

    的值了
8.3、撤销偏向锁-发生锁竞争 (升级为重量级锁)
小故事: 线程A门上刻了名字, 但此时线程B也要来使用房间了, 所以要将偏向锁升级为轻量级锁. (线程B要在线程A使用完房间之后

(执行完synchronized代码块)

,再来使用; 否则就成了竞争获取锁对象, 此时就要升级为

重量级锁

了)
偏向锁、轻量级锁的使用条件, 都是在于多个线程没有对同一个对象进行

锁竞争

的前提下, 如果有

锁竞争

,此时就使用重量级锁。
  • 这里我们演示的是

    偏向锁

    撤销, 变成

    轻量级锁

    的过程,那么就得满足轻量级锁的使用条件,就是没有线程对同一个对象进行

    锁竞争

    ,我们使用

    wait

    notify

    来辅助实现
  • 虚拟机参数

    -XX:BiasedLockingStartupDelay=0

    确保我们的程序最开始使用了

    偏向锁

Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
  • 输出结果,最开始使用的是

    偏向锁

    ,但是第二个线程尝试获取对象锁时(前提是: 线程一已经释放掉锁了,也就是执行完synchroized代码块),发现本来对象

    偏向的是线程一

    ,那么偏向锁就会失效,加的就是

    轻量级锁

    Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
8.4、撤销偏向锁 - 调用 wait/notify (只有重量级锁才支持这两个方法)
(调用wait方法会导致锁膨胀而使用重量级锁)
  • 会使对象锁变成重量级锁,因为

    wait/notify方法之后重量级锁才支持

9、批量重偏向

  • 如果

    对象被多个线程访问,但是没有竞争 (上面撤销偏向锁就是这种情况: 一个线程执行完, 另一个线程再来执行, 没有竞争)

    , 这时

    偏向T1的对象仍有机会重新偏向T2

    • 重偏向会重置Thread ID
  • 撤销偏向锁101 升级为 轻量级锁00

    超过

    20次后(超过阈值)

    ,JVM会觉得是不是偏向错了,这时会在

    给对象加锁时,重新偏向至加锁线程 (T2)。

9.1、批量撤销偏向锁
  • 当 撤销偏向锁的阈值超过40以后 ,就会将整个类的对象都改为不可偏向的
9.2、同步省略 (锁消除)
同步省略
  1. 线程同步的代价是相当高的,同步的后果是

    降低并发性和性能

  2. 在动态编译同步块的时候,

    JIT编译器

    可以

    借助逃逸分析

    来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。
  3. 如果没有,

    那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。

    这个取消同步的过程就叫同步省略,也叫锁消除。
  • 例如下面的智障代码,

    根本起不到锁的作用

public void f() {
    Object hellis = new Object();
    synchronized(hellis) {
        System.out.println(hellis);
    }
}
           
  • 代码中对hellis这个对象加锁,但是hellis对象的生命周期只在f()方法中,并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉,优化成:
public void f() {
    Object hellis = new Object();
	System.out.println(hellis);
}
           

字节码分析

  • 代码
public void f() {
    Object hellis = new Object();
    synchronized(hellis) {
        System.out.println(hellis);
    }
}
           
  • 注意:字节码文件中并没有进行优化,可以看到加锁和释放锁的操作依然存在,

    同步省略操作是在解释运行时发生的

Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念

继续阅读