早期的synchronized都是重量级实现,但因重量级锁比较笨重,涉及用户态到内核态的切换,所以后期jdk对sync做了优化,使得在没必须使用重量级锁的情况下,不加锁或加性能稍好的轻量级锁。

预备知识

对象头:

对象头有Markword (8字节)+classpointer(4字节)组成。
MarkWord存gc分代年龄、锁信息、hashcode
classpointer存指向类信息的对象引用

CAS: 自旋锁

Monitor

同步机制。每一个java对象有一把看不见的锁,称为内部锁或monitor锁。
monitor是线程私有数据结构,每一个线程都有一个可用的monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor有一个onwer字段存线程的惟一标识。

锁升级

锁升级过程主要有4个阶段:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
image.png
【图1】

升级过程image.png

图【2】 锁升级

无锁

无锁是指没有对资源上锁。没有线程访问对象的同步内容。

偏向锁

在没有锁竞争的情况下,资源只被某个线程访问,此时sync将会把无锁状态升级为偏量级锁,具体操作是在对象头markword中记录线程id. 此后该线程进出对象同步内容时,并不需要加锁解锁。而其线程访问该同步内容时,因偏向线程id不匹配,需要升级锁才能继续访问。
偏向锁默认开启,可通过-XX;UseBiasedLocking=false

偏向锁撤销安全点

而偏向锁的撤销,且需要等待全局安全点,即在某个时间点上没有字节码正在执行时,它会先暂停拥有偏向锁的线程,然后判断对象是否处于被锁定状态。如果 没有,则将对象头设置为无锁状态,并撤销偏向锁,恢复到无锁或升级轻量级锁。

轻量级锁

轻量级锁的本质就是cas自旋锁。
轻量级锁获取主要的两种方式:

  1. 当存在多线程竞争一个对象的同步内存时,会从偏向锁升级为轻量级锁
  2. 当关闭偏向锁功能时

当代码进入同步块的时候,如果对象锁状态为无锁状态,虚拟机会首先在当前线程的栈帧中建立 一个名为LockRecord的空间,然后将Markword复制到LockRecord中。
copy成功后,虚拟机将使用cas将对象markword更新为指向lockRecord的指针,并将lockrecord的owner指向对象的markword.
如果更新成功,那么这个线程就拥有了该对象的锁,并且对象md标志位设置为00,表示对象处于轻量级锁状态。
如果更新失败,jvm会首先检查对象md是否指向当前线程的栈帧,如果是说明已拥有对象锁,就可以访问对象的同步内容了,如果否说明多个线程竞争锁。


什么时候锁膨胀?
  1. 若当前只有一个等待线程,则该线程将通过自旋进行等待 。但是当自旋超过一定次数,就会升级为重量级锁(锁膨胀)

1.6 之前,自旋锁默认10次,超出10次没成功就会将线程挂起。可通过-XX:PreBlockSpin=10来修改。
jdk1.6+ : 加入自适应自旋转锁,

  1. 自旋线程数超过cpu核心数的一半。

重量级锁

重量级锁当一个线程获取锁以后,其他所有等待获取该锁的线程都处于阻塞状态。
重量级锁通过对象内部的监控器实现,而其中的minitor的本质依赖于底层操作系统的Mutext Lock实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。也就是说由操作系统 来负责线程间的调度和线程的状态变更。而这样会出现频繁的线程状态切换,消耗大量系统资源导致性能下降。

锁升级的相关重要问题

加锁后对象头的hashcoe等数据去哪了?

如【图1】:在有锁状态下hashcode、分代年龄以及重入次数去哪了?
线程栈有一个lockrecord 这个东西,它是一个栈结构,用来每加锁一次push一个lr
偏向锁:线程栈的LockRecord里(lc[0]栈里第一个lockrecod里保存了dispalyed markword加锁前旧的markword数据的备份)
轻量级锁:线程栈LockRecord里
重量级锁:ObjectMonitor类会记录hashcode。
且HotSpot VM是假定“实际上只有很少对象会计算identity hash code”来做优化的。言外之意,之于性能,在有锁状态下markword上存线程信息比直接存hashcode重要。

为什么有了自旋锁还需要重量级锁?

竞争激烈时,消耗CPU。

偏向锁一定比自旋锁高效吗?

不一定,在明确知道会有多线程竞争的情况下,没必要再加偏向锁了,因为偏向锁撤销也需要时间。这时候直接获取轻量级锁就可以了。
在jvm启动过程中,会有很多线程竞争,所以-XX:BiasedLocingStratupDelay=4 默认延迟4秒才启动偏向锁。

什么是匿名偏向锁?

已知会加偏向锁,但还没有偏向于哪个线程就会加匿名偏向锁。

什么情况下偏向锁会直接升级为重量级锁?如图2

重度竞争:耗时过长,调用object.wait()等

锁重入

sync是可重入锁。
重入锁次数必须记录,因为几次加锁得对应几次解锁。
偏向锁:自旋转锁->线程栈->LR+1
重量级锁:对象的objectMonitor字段上。

重量级锁

  1. ObjectMonitor() {
  2. _count = 0; //锁计数器
  3. _owner = NULL;
  4. _WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
  5. _EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
  6. }

image.png
sync代码块编辑后,前后会加monitorenter和monitorexit
每进入monitorenter _count+1

  1. 当多个线程进入同步代码块时,首先进入entryList
  2. 有一个线程获取到monitor锁后,就赋值给当前线程,并且计数器+1
  3. 如果线程调用wait方法,将释放锁,当前线程置为null,计数器-1,同时进入waitSet等待被唤醒,调用notify或者notifyAll之后又会进入entryList竞争锁
  4. 如果线程执行完毕,同样释放锁,计数器-1,当前线程置为null

参考资料

深入分析Synchronized原理(阿里面试题)