对Synchronized锁升级,锁消除的理解


前言

Synchronized 锁升级放下这么久了,今天打算好好学一下。主要方式是通过读《Java 并发编程的艺术》来进行学习。

Synchronized 锁状态共分为无锁、偏向锁、轻量级锁以及重量级锁。锁一旦升级就不能进行降级

先从 Synchronized 锁对对象的影响开始

先看一下用来判断一个对象是什么的对象头(Mark Word)。

锁状态 25bit 4 bit 1 bit 用来判断是否为偏向锁 2bit 锁标识
无锁状态 hashCode 对象分代年龄 0 01

如表所示,这是一个”未被锁的对象”,其对象头存储了 hashCode ,也就是调用 == 时判断对象是否为同一个对象的时候用到的值,原来对象在新建的时候就已经设置好了 hashcode,不是用到的时候再计算的。

然后就是 4 bit, 我们都知道 GC 垃圾回收过程中,一般对象刚分配会分到新生代 Eden,在多次存活后(先放到 Suvivor 区),当年龄达到后,就会移交到老年代,这样就能减少对新生代进行垃圾回收时的性能消耗。每次存活对象的年龄都会增加,原来就是存储在这里啊。

然后就是判断偏向锁的标识,很明显就是判断是否给当前对象加了偏向锁,最后的锁标识则是用来标识当前对象被锁的状态到底是什么锁,是偏向锁还是、轻量级锁或者重量级锁。

偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程获得。为了减少线程获取锁以及释放锁带来的性能消耗,引入了偏向锁。

意思很明显,就是让这个对象偏向于被某个线程加锁。

实现原理:

锁状态 23bit 2bit 4bit 1bit 是否是偏向锁 2bit 锁标识位
偏向锁 偏向线程 ID 偏向时间戳 对象分代年龄 1 01

当一个线程访问对象获取锁的时候,会判断这个对象的对象头中的偏向锁标识是否为 1

  • 如果为 0,则说明未被加锁,于是设置为 1,并把偏向线程 ID 设置为自己的 ID。执行完后不释放锁,下次再访问到这个代码块的时候,判断这个线程 ID 是否是自己的 ID,如果是,由于没有释放锁,则直接执行代码块
  • 如果为 1,说明被加锁,判断是偏向锁 ID 是否为自己的 ID,如果是则直接执行,因为自己已经获取了锁。如果不是自己的 ID,则尝试用 CAS 修改线程 ID 为自己的。

此时重点来了,线程尝试修改 CAS 修改线程 ID 为自己的 ID 时,修改失败。也就是说明其他线程还用着呢,你怎么就要获取了,这里就发生了锁的竞争

因此会发生偏向锁的撤销(偏向锁标识设置为 0,锁标识不变,因为无锁和偏向锁标识一样),然后进一步升级为轻量级锁

画了个流程图:

轻量级锁(自旋锁)

既然已经到了轻量级锁,说明出现了竞争。

从加锁和解锁两部分说明。

  1. 加锁
    线程通过 CAS 修改对象头内的锁标识
  • 如果当前锁标识为无锁状态,则直接获取锁,并修改标识为有锁状态。
  • 如果锁标识为有锁,则等待其他线程释放锁,并频繁的进行”自旋操作”尝试获取锁。
  1. 释放锁
    设置标识为无锁状态。

这样有什么问题?

  • 造成其他线程一直空转(不执行任务,但占用 CPU)并进行自旋操作,造成资源浪费,如果一直获取不到就会一直等待。

因此轻量级锁适合于锁竞争不激烈的情况,如果锁竞争太过激烈(比如自旋次数超过 10 次),就需要升级为重量级锁。

重量级锁

其他线程在获取锁失败时发现是重量级锁,则不会等待,而是直接挂起,等待获取锁的线程执行完之后进行唤醒,其他线程才继续开始获取锁,并执行代码。

由此可以看到重量级锁,避免了线程空转,减少了 CPU 消耗,但是想对的,线程被阻塞,性能变慢。


文章作者: KTpro
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 KTpro !
评论
  目录