锁的升级与对比

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

Java对象头(在内存中的存放,和Class文件的布局没什么关系)

HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
HotSpot虚拟机的对象头(Object Header)包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64个Bits,官方称它为“Mark Word”。

锁存在Java对象头里。如果对象是数组类型,则虚拟机用3个Word(字宽)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,一字宽等于四字节,即32bit。

长度 内容 说明
32/64bit Mark Word 存储对象的hashCode或锁信息等
32/64bit Class Metadata Address 存储到对象类型数据的指针
32/64bit Array length 数组的长度(如果当前对象是数组)

Java对象头里的Mark Word里默认存储对象的HashCode,分代年龄和锁标记位。32位JVM的Mark Word的默认存储结构如下:

25 bit 4bit 1bit是否是偏向锁 2bit锁标志位
无锁状态 对象的hashCode 对象分代年龄 0 01

在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下4种数据:

  • 偏向锁,轻量级锁都是乐观锁,重量级锁是悲观锁

首先,应该知道三种锁偏向锁,轻量级锁,重量级锁的应用场景

  • 偏向锁主要用于只有一个线程进入临界区的情况。
  • 轻量级锁主要用于两个线程竞争程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁。
  • 重量级锁就是用于竞争十分强烈的情况下。

偏向锁(锁标志位01)

当Thread#1进入临界区时,JVM会将lockObject的对象头Mark Word的锁标志位设为“01”,同时会用CAS操作把Thread#1的线程ID记录到lockObject的对象头Mark Word中,此时进入偏向模式。所谓“偏向”,指的是这个锁会偏向于Thread#1,若接下来没有其他线程进入临界区,则Thread#1再出入临界区无需再执行任何同步操作。也就是说,若只有Thread#1会进入临界区,实际上只有Thread#1初次进入临界区时需要执行CAS操作,以后再出入临界区都不会有同步操作带来的开销。

(也就是在锁住的对象的对象头上记录下线程的ID,这样下次该线程再次出入该锁住对象管理的临界区,只需要验证一下对象头上的线程ID即可)

1
2
3
4
5
一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。

一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。

轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

​ HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

​ 也就是说,大多数情况下的锁都要通过CAS竞争来获取,偏向锁的目的就是为了消除CAS竞争的开销。直接在对象头标记,下次获取锁时直接看标记就好了,不用再CAS修改获取了,所以直接说省去了CAS 竞争锁的开销。原先的CAS修改需要先看锁是不是在,compare后发现不在,然后再修改锁的值,变成有锁。现在偏向锁直接去掉了CAS修改步骤,只需要CAS替换Mark Word就好了。

偏向锁的撤销

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。图中的线程1演示了偏向锁初始化的流程,线程2演示了偏向锁撤销的流程。

轻量级锁(锁标志位00)

在代码进入同步块的时候,如果此同步对象没有被锁定,(锁标志位为01状态)虚拟机首先将当在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,这个空间用于当前线程去 存储LockObject(锁住对象)目前的Mark Word的拷贝。(官方称为Displaced Mark Word)。

然后虚拟机将使用CAS操作尝试将对象的Mark Word 更新为指向Lock Record 的指针,如果这个更新操作成功了,就代表这个线程拥有了该对象的锁。并且对象Mark Word 的锁标志位也将转变为00. 即表示此对象处于轻量级锁定状态,

如图所示:

如果这个更新操作失败了,虚拟机先去检查Mark word是否指向当前线程的栈帧,如果只说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明这个锁对象已经被其他线程抢占了。如果有两个以上线程争用同一个锁,那么轻量级锁就不再有效,要膨胀为重量级锁。

通过上面的描述可知,轻量级锁是用CAS进行加锁的,其实,解锁也是使用CAS操作的。如果对象的Mark Word仍然指向线程的锁记录,那就用CAS操作把对象当前的Mark Word 和线程中复制的 Displaced Mark Word 替换回来。替换成功的话整个同步过程就完成了,替换失败的话,说明有其他线程尝试获取该锁,就要在释放锁的同时,唤醒被挂起的线程。

轻量级锁使用CAS操作避免了使用互斥量的开销,所以可以在一定程度上提升性能。

https://blog.csdn.net/choukekai/article/details/63688332