在JDK1.6以前,使用synchronized就只有一种方式即重量级锁,而在JDK1.6以后,引入了偏向锁,轻量级锁,重量级锁,来减少竞争带来的上下文切换。

一、Java对象头

每个对象都拥有对象头,对象头由Mark World ,指向类的指针,以及数组长度三部分组成Mark World记录了对象和锁有关的信息,在64位JVM中Mark World的长度是64bit具体结构如下:
image.png
锁升级主要依赖Mark Word中的锁标志位和释放偏向锁标识位。一般的synchronized同步锁升级步骤是:
偏向锁 -> 轻量级锁 -> 重量级锁

二、Synchronized内核态切换

简单来说在JVM中monitorenter和monitorexit字节码依赖于底层的操作系统的Mutex Lock来实现的,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的。

三、锁升级优化

  1. 偏向锁

偏向锁的作用是当有线程访问同步代码或方法时,线程只需要判断对象头的Mark Word中判断一下是否有偏向锁指向线程ID.偏向锁记录过程:

  • 线程抢到了对象的同步锁(锁标志为01参考上图即无其他线程占用)
  • 对象Mark World 将是否偏向标志位设置为1
  • 记录抢到锁的线程ID
  • 进入偏向状态

偏向锁的优势:通过加偏向锁的方式可以看到,对象中记录了获取到对象锁的线程ID,这就意味如果短时间同一个线程再次访问这个加锁的同步代码或方法时,该线程只需要对对象头Mark Word中去判断一下是否有偏向锁指向它的ID,不需要在进入Monitor去竞争对象了。

  1. 轻量级锁

当有另外一个线程竞争获取这个锁时,由于该锁已经是偏向锁,当发现对象头 Mark Word 中的线程 ID 不是自己的线程 ID,就会进行 CAS 操作获取锁,如果获取成功,直接替换 Mark Word 中的线程 ID 为自己的 ID,该锁会保持偏向锁状态;如果获取锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。
JVM 提供了一种自旋锁,可以通过自旋方式不断尝试获取锁,从而避免线程被挂起阻塞。这是基于大多数情况下,线程持有锁的时间都不会太长,毕竟线程被挂起阻塞可能会得不偿失。
从 JDK1.7 开始,自旋锁默认启用,自旋次数由 JVM 设置决定,这里我不建议设置的重试次数过多,因为 CAS 重试操作意味着长时间地占用 CPU。自旋锁重试之后如果抢锁依然失败,同步锁就会升级至重量级锁,锁标志位改为 10。在这个状态下,未抢到锁的线程都会进入 Monitor,之后会被阻塞在 _WaitSet 队列中。

  1. 重量级锁

自旋锁重试之后如果抢锁依然失败,同步锁就会升级至重量级锁,锁标志位改为 10。在这个状态下,未抢到锁的线程都会进入 Monitor,之后会被阻塞在 _WaitSet 队列中。

总结:
偏向锁:适用于单线程适用锁的情况
轻量级锁:适用于竞争较不激烈的情况(这和乐观锁的使用范围类似)
重量级锁:适用于竞争激烈的情况