Synchronized锁优化浅析

引言

Synchronized作为互斥锁的实现,使用简单,但却低效,重量级锁也因为性能低效得原因而得名,并且在JDK1.5发布后有被RetreenLock替代的可能。但随着JDK1.6发布中对Synchronized锁进行了优化,使的Synchronized的性能有了明显的提升。

为什么Synchronized性能差

我们都知道,Synchronized锁性能差的原因是由于线程在切换过程中需要进行上下文切换,上下文切换的过程是用户态和内核态切换过程中线程信息的获取与保存的过程。因此,在线程激烈竞争锁过程中,频繁的上下文切换,必然会造成系统性能骤降的问题。

Java线程实现模型

在Unix系统中,Java的线程采用内核线程(kernel thread)的实现方式。内核线程运行在内核态,应用进程无法直接使用。需要通过内核的抽象对外接口”轻量级进程“(LWP)来完成。轻量级进程运行在用户态,一个进程可以关联多个轻量级进程,每个轻量级进程与一个特定的内核线程进行关联。

一对一线程模型

这种实现模式称为“内核级线程模式”,在内核级模式下,线程的创建、调度、销毁都需要由操作系统来负责,通过系统调用完成。系统调用是需要消耗大量的系统资源的,这也就说明了为什么频繁的线程切换会导致应用的性能下降了。

Synchronized性能数据

Synchronized jdk1.5性能数据

Synchronized锁优化

知识点

知识点 说明
CAS 比较并替换(Compare and Swap),是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作。该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。
对象头 对象头是对象在内存中的其中一个部分,这个部分又分别由Mark Work和类型指针组成。
Mark Word 用于存储对象自身运行时的数据,包括hashcode、GC分代年龄、锁状态标记、偏向锁ID、偏向时间戳等数据,Mark Work占用的内存与虚拟机位长保持一致。

Mark Word的实现

JVM设计者为了在极小的空间中能尽可能多的保留对象运行时的数据,所以将Mark Work设计成非固定的数据结构,以32位Mark Work实现为例,各种锁对应的组成结构如下图


mark word组成

Synchronized的锁优化,主要是基于Mark Word与CAS来完成的。

锁优化策略

JVM设计者将Synchronized锁根据线程竞争激烈程度与系统资源消耗程度,将锁由轻到重分为4种等级(无锁->偏向锁->轻量级锁->重量级锁)。

偏向锁(Biased Lock)

针对无线程竞争的场景设计,尽量避免不必要的原子操作(轻量级锁需要多次CAS操作),偏向锁只需要线程首次获取锁时执行一次CAS操作,将线程ID设置到Mark Word中。后续通过比较Mark Word中的线程id,如果匹配直接进入同步逻辑。在偏向锁模式下,当发生其他线程尝试获取锁资源时,就会触发锁撤销操作。 将锁设置为无锁不可偏向或轻量级锁模式。锁撤销并不会马上执行,而是需要持有偏向锁的线程执行到安全点并挂起线程后,锁才能执行撤销操作。轻量级锁默认是开启的,可以通过设置JVM参数-XX:-UseBiasedLocking关闭偏向锁设置。
对象头Mark Word中专门使用一个bit来表示对象是否支持偏向锁,如果这个标识位为0,表示该对象处于无锁并且不支持偏向锁的;如果标识位为1,则表示支持偏向锁,偏向锁可以细分为以下三种状态:

状态 描述
匿名可偏向(Anonymously biased) 这种状态表示没有线程获取过该对象锁的偏向锁初始状态。当第一个线程尝试获取锁时,只需要通过一个CAS操作自身线程ID设置到Mark Word中。
已偏向(Biased) 当前Mark Word中线程id不为空,且偏向时间戳(Epoch)有效。表示线程当前仍持有锁。
可重偏向(Rebiasable) 当前Mark Word中的偏向时间戳(Epoch)已经失效。在执行批量重偏向操作时,如果偏向锁没有再被线程持有的话,就会将锁设置为可重偏向状态。
偏向锁的组成

偏向锁中mark word组成结构

JVM设计者针对所有已加载的类,都会在它的元数据(class metadata)中保存一个原始的Mark Word(prototype Mark Word),prototype Mark Word保存着可偏向状态(bias status)和偏向时间戳(epoch)。所有新创建的对象,都会直接将prototype mark word中的属性继承下来并直接使用。

偏向锁获取过程
image.png

偏向锁获取过程可以大致分为
1.检查object mark word中bias状态位,状态为1时表示支持偏向锁,状态为0时表示对象不支持偏向锁,只能使用轻量级锁算法。
2.检查class prototype mark word中bias状态位,如果class的bias设置为0,表示全局(所有object)不支持偏向锁。
3.检查epoch时间戳,如果epoch为0说明对象还没被执行过偏向,这个时候执行步骤5。如果发现epoch不为0,就会将对象的epoch与类中prototype epoch做比较,发现不一致的话,线程尝试执行步骤5,如果成功,就完成偏向锁转移。如果失败,执行步骤4。
4.比较mark word的线程id,如果匹配,说明是同一个线程重新获取对象锁,此时只需要简单更新epoch属性即可。如果线程id不匹配,线程会尝试执行步骤5,成功的话,完成偏向锁转移;如果失败,则线程执行锁撤销。(撤销过程是一个批量处理过程)。
5.通过CAS的方式设置对象mark word的Thread refrence、epoch属性。
6.CAS执行成功后,将线程栈中对象指针指向对象。
整个获取偏向锁过程完成,其中步骤3,4在特殊场景下会有执行锁重偏向与撤销逻辑。

偏向锁重偏向与撤销
偏向锁重偏向与撤销状态转换图

下面以线程A尝试获取线程T持有对象锁为例说明整个偏向锁重偏向及撤销过程。

1.当线程A在获取锁过程中,发现当前对象已经处于偏向状态,首先会直接尝试通过CAS方式来获取对象锁,如果成功,说明线程T已经释放(unlock)了对象锁,此时,对象锁完成重偏向到新线程的过程(rebias to bias)。
2.如果执行CAS失败,则进入偏向锁撤销环节。偏向锁撤销操作需要在全局安全点内将所有线程暂停后才执行,如果只是为了撤销单一的锁偏向,成本太高,所以JVM设计者采用批量锁撤销的方式。
3.当到达全局安全点,线程都挂起后,JVM的线程会扫描对象mark word和线程栈中所有锁记录。
4.在扫描线程T的栈中锁记录时,如果发现当前锁记录的对象引用已经不在指向对象(对象mark word中的thread refrence此时还是保存线程T的ID),则根据撤销的原因,执行重偏向无锁不可偏向操作。
5.重偏向(re-bias):仅当线程T释放对象锁,无发生锁竞争时执行重偏向逻辑。主要执行逻辑将对象mark word的epoch设置为无效
6.无锁(Unlock):发生线程T持有锁时线程A同时获取锁,当达到线程安全点时,线程T已经释放了对象锁,此时将对象mark word设置为无锁,不可再偏向状态。
7.JVM线程在执行扫描过程中,仍发现线程T持有锁,并且线程A有尝试过获取锁,此时执行锁升级操作,升级为轻量级锁算法。

轻量级锁(Thin Lock)

轻量级锁可以认为是一种自旋锁,通过CPU轮询执行操作系统的原子操作(在Java中采用CAS的方式)来实现锁得获取与释放。自旋锁避免了互斥锁因线程间上下文切换带来的性能问题,在竞争不算激烈的情况下,可以获得很好的性能。但自旋锁也存在不足,如果线程长时间获取不到锁资源,将一直占用cpu资源,导致cpu资源利用率低下的问题。
轻量级通过执行一定次数的自旋后,如果仍然无法获取锁资源,那将执行膨胀操作,升级为互斥锁。

轻量级锁获取锁过程

获取锁过程中线程栈与对象头存储变化

1.将对象Mark Word数据结构(hashcode、gc age...)通过CAS原子操作设置到线程栈锁记录(Lock Record)的displaced hdr中。
2.将对象Mark Word的锁状态(lock flag)设置为00(代表轻量级锁)。
3.在对象Mark Word中保持指向线程锁记录的栈指针(stack refrence),同时将线程锁记录的对象引用 (object refrence)执行对象。完成一个双向引用操作。

一个完整的轻量级锁获取过程,起码需要经历4次CAS操作。所以,获取轻量级锁的CAS次数总是>=4的。如果出现线程持续获取锁失败的情况,那么,轻量级锁就会执行膨胀,意思就是升级为重量级锁。

轻量级锁升级重量级锁过程

膨胀过程线程栈与对象数据结构的变化

1.由轻量级锁膨胀到重量级锁需要经历一个INFATING的状态。INFATING状态首先会将对象Mark Word存储栈指针(stack refrence)设置为NULL(0),处于这个状态下的对象锁是无法被释放的,目的就是为了保证整个膨胀到重量级锁的过程中Mark Word是不会发生变化的。
2.将Mark Word的锁状态设置为10(代表重量级锁)。
3.创建Monitor Object, 将原保存在线程栈中的displaced mark word设置给Monitor Object,并且清理掉线程栈中的displaced hdr数据结构。
4.将对象Mark Word中的NULL(0)设置为一个指向montor object的指针。

到此,轻量级锁完成膨胀为重量级锁的过程。后续线程再发生竞争,将会进入线程间上下文切换的锁模式。

你可能感兴趣的:(Synchronized锁优化浅析)