synchronized锁升级

148

锁的原理

随着java1.6对synchronized的优化,减少了获取锁和释放锁带来的性能消耗而引入了偏向锁和轻量级锁,有些情况下它就不会那么重,锁的状态分为4种:无锁、偏向锁、轻量级锁、重量级锁,这几种状态会逐渐升级

对象在内存中的存储布局可以分为对象头、实例数据、对齐填充,对象头中就包含Mark Word,以下是64位Hotspot虚拟机的Mark Word存储结构

查看对象的Mark Word,可以通过在项目中引入ClassLayout

锁状态

Mark Word

25位

31位

1位

4位

1位(是否偏向锁)

2位(锁标志位)

无锁

unused

hashcode

unused

分代年龄

0

0

1

  1. 偏向锁 在一些情况下锁总由一个线程多次获得,而不会发生竞争

在锁对象的对象头记录当前获取该锁的线程id,当这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁.而是直接比较对象头里面是否存储了指向当前线程的线程id.如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了

不过如果加锁的对象调用了hashcode方法(如手动调用或者该对象存入hashmap),则会跳过偏向锁直接通过轻量级锁加锁,因为偏向锁没有Displaced Mark word

偏向锁会通过对象头的偏向线程id和栈帧中lock record的线程id匹配直接获取锁,否则使用轻量级锁

锁状态

Mark Word

54位

2位

1位

4位

1位(是否偏向锁)

2位(锁标志位)

偏向锁

偏向线程id

偏向锁时间戳

unused

分代年龄

1

0

1

  1. 轻量级锁 当两个及以上的线程交替获取该锁,但并没有并发获取锁时,偏向锁升级为轻量级锁,线程采用CAS自旋的方式获取锁,避免阻塞线程造成的cpu上下文切换导致的消耗

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间 (Lock Record记录),并将对象头中的Mark word复制到锁记录中,官方称为Displaced Mark word.然后线程尝试使用CAS将对象头中的Markword替换为指向锁记录的指针 (指向线程栈帧里边的Lock Record的指针).如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁

轻量级解锁时,会使用CAS操作将Displaced Mark word替换回到对象头,如果成功则表示没有竞争发生.如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁

Mark Word中的内容只是暂时被锁指针占用,临时存储在栈中,并不会丢失

锁状态

Mark Word

62位

2位(锁标志位)

轻量级锁

指向线程栈的Lock Record锁记录的指针

0

0

重量级锁

指向重量级锁的指针

1

0

如果锁标志为11,代表被jvm被标记为垃圾的对象

  1. 重量级锁 当两个及以上的线程并发的在一个对象上同步时,为了避免面自旋消耗cpu资源,轻量级锁升级为重量级锁.当轻量级锁升级为到重量级锁之后,线程只能被挂起阻塞来等待唤醒了

重量级锁需要操作系统的介入,JVM会创建一个monitor对象,并把这个对象的地址更新到Mark Word中

流程总结

  • 线程1目前为轻量级锁,进入同步代码块,分配空间并记录对象的Mark Word,通过CAS修改Mark Word成功.将该对象Mark Word改为轻量级锁指针

  • 线程1执行同步代码

  • 线程2尝试进入同代码块,和线程1一样希望将对象的Mark Word记录到栈中,但是通过CAS修改Mark Word失败,发现已经被其他线程替换过了.线程2将该对象的Mark Word再次修改为临时的指向重量级锁的指针(锁膨胀)

  • 线程1执行完同步代码,将栈内存储的Mark Word改回原本存储的(分代年龄、hashcode等),替换失败(因为被线程2修改为重量级锁的指针),释放锁并唤醒等待线程

  • 线程1初始化monitor对象,将monitor内的header属性设置为自己栈中存储的Mark Word

  • 线程1设置锁标志为10,然后将Mark Word前62位设置为指向重量级锁的指针,唤醒其他线程

  • 线程2被唤醒开始争抢重量级锁(非公平锁)

线程2只是设置了一个临时的重量级锁标记,重量级锁指针是由线程1初始化的,线程2做的只是为了告知此同步代码块产生了并发

节码指令

通过 javap -p -c 查看字节码指令,可以看到每个monitorenter指令分别对应了2个monitorexit指令

  • 当正常执行释放锁时会执行第一个monitorexit并通过goto指令跳过第二个monitorexit部分

  • 当出现异常时,虚拟机会通过异常处理表,也就是会执行第二个monitorexit,防止出现异常情况jvm也能释放锁

ObjectMonitor

只有锁升级为重量级锁时才会创建ObjectMonitor,它有如下属性

  • header 保存对象Markword

  • own 指向锁持有的线程

  • cxq 竞争队列

  • entryList 同步队列

  • waitset 等待队列

当A线程持有锁,BC线程竞争失败并进入cxq,当A线程释放锁后,BC线程会进入entryList中.假设B线程竞争到了锁并同时调用了Object.wait方法会进入到waitset.当C线程拿到锁后唤醒了B线程,B线程会从waitset中出来,直接竞争锁.如果竞争失败进入cxq继续轮回