JVM锁:synchronized原理详解

JVM锁:synchronized原理详解

本文整理本人对synchronized关键字的个人理解,增加自身对synchronized的理解与印象之外,也希望能对同样存在疑惑的你有所帮助。


文章目录

  • JVM锁:synchronized原理详解
  • 前言
  • 一、对象在JVM的存储方式
    • 1.JVM运行时数据区
    • 2.对象内存中的布局
  • 二、锁状态的记录
  • 三、锁升级
  • 后记


前言

首先我们需要整理知道一些基本背景:
1.锁到底是什么?
就本人理解来说,锁是其实是程序多并发过程中为了保证线程安全原子性的一种方式,锁的本质其实是对资源操作权限,只有获取到权限的线程才能对资源进行相关操作。
2.锁的概念即分类有哪些?
根据不同的分类方式,锁的分类自然也就有很多种:自旋锁,乐观锁|悲观锁,独享锁(互斥锁)|共享锁,可重入锁|不可重入锁。
3.synchronized关键字的特性是什么?
synchronized关键字是jvm提出的,lock是jdk提出的,因此synchronized提出的时间也就更早些。synchronized属于可重入锁,独享锁,悲观锁。


为了理解synchronized的原理,我们可以从一下几个问题来入手:

//锁住this对象
synchronized(this) {
	i++;
}

1.加锁的状态是如何记录的?
2.状态会被记录到this对象中吗?
3.锁若占用,线程挂起;释放锁时,唤醒挂起的线程是如何做到的?

一、对象在JVM的存储方式

1.JVM运行时数据区

JVM运行时数据区可以分为:线程共享内存和线程独占内存。
线程共享内存有:方法区,堆
线程独占内存有:虚拟机栈,程序计数器,本地方法栈。
JVM锁:synchronized原理详解_第1张图片

2.对象内存中的布局

一个对象的属性和变量会分布在不同的内存中,如局部变量分布在线程独占内存,对象属性分布在堆内存中,类属性则分布在方法区中。那么内存中的对象又是如何判断是属于什么类的对象呢?其实在每个对象内部会存在一个对象头,在对象头中有一个Class Address属性,它会指向方法区中对象对应类的内存地址。
JVM锁:synchronized原理详解_第2张图片
从图中我们可以发现,一个对象的对象头中除了指向对象类内存地址的Class Address之外,还有两个属性,分别是Array Length, Mark Word。
Array Length用于存储对象为数组对象时数组的长度,Mark Word是一段内存,内存长度为32位或64位,这个由系统决定,如果系统是32位的那么Mark Word内存长度就是32位,反之64位系统,它的长度就是64位。
那么Mark Word是用来做什么的呢?其实他就是用来存储对象状态的,即锁的状态。

二、锁状态的记录

现在我们知道每个对象都存在一个对象头,而对象头中的Mark Word属性就是用来存储锁的状态的,那么它的数据结构又是什么样子的呢?
通过HotSpot JVM的相关文档我们可以找到Mark Word数据结构如下:
JVM锁:synchronized原理详解_第3张图片
为什么会有这么多行呢?这是因为当前对象的状态不同,即锁状态不同,从表中我们也可以发现对象的锁状态可以分为:未锁定,轻量级锁,重量级锁,锁解锁,以及偏向锁。其中偏向锁又分为两种状态,即偏向锁开启未锁定,偏向锁开启已锁定。

三、锁升级

现在我们已经知道了对象加锁时,锁的状态被记录在对象对象头的Mark Word中,并且在Mark Word中会记录当前对象的锁状态,那么这个锁状态又是怎么转化的呢。
JVM锁:synchronized原理详解_第4张图片
从图中我们可以发现,对象在创建后是未锁定状态的,线程对对象加锁的过程其实也就是对象Mark Word修改的一个过程:
step1.首先线程会虚拟机栈中创建一个Lock Record,用于存储对象Mark Word的当前值。
step2.通过CAS操作将对象Mark Word的值进行修改,改为当前线程的地址,也就是Lock record address.
在这个时候对象的锁也就加上了,也就是轻量级锁。
step3.如果进行加锁的操作的时候存在多个线程一起操作,但是能加锁成功的线程只能是一个,那么其他线程在抢锁失败后,并不会进行阻塞,而是会进行自旋,不断的去抢锁,直到能抢到锁才会结束。
step4.但是这样不断自旋CAS抢锁的行为其实是非常损耗性能的,所以jvm就又提出了当自旋的次数达到一个的阈值后,便会再次进行锁升级,升级为重量级锁。
step5.当升级为重量级锁时,MarkWord的内容就有发生了变化,为了保障性能,jvm会对每个对象生成一个Object Monitor用于存储对象锁的相关信息,在Object Monitor中会存储对象owner的地址引用,也就是当前对象锁的拥有者的地址(即Lock record address),此外,会将抢锁失败的线程,放入到锁池中(entryList),并让相关线程进行挂起,线程阻塞,防止继续自旋消耗性能。
step6.当拥有锁权限的线程结束了相关操作,则在锁池中的线程会再次进行抢锁。
step7.若锁的owner线程调用wait方法,则会释放锁,同时会进入Object Monitor的等待池(waitSet)中,等待被下一位owner唤醒(notify),从而从等待池中唤醒,进入锁池再次进行抢锁操作。
step8.锁只存在升级,不存在降级的情况,因此重量级锁之后,便是解锁操作了。(一个锁的闭环也就完成了)
说到这里,大家可能发现了,不是还有一个偏向锁吗?没错,还有一个偏向锁,这个偏向锁其实也是jvm为了在某些情况下做的关于锁的性能优化的操作。
在这里插入图片描述
首先我们需要知道,偏向锁是可以通过配置参数进行开启和关闭的。知道了这个,也就可以知道为什么在之前的锁升级的图中会出现两种“未锁定”的状态了。那么偏向锁是怎么达到优化性能的目的的呢。其实在jvm中开启偏向锁后,如果只有一个线程进行加锁操作,那么我们可以认为其实这个锁是没有必要的,毕竟锁的存在的意义也就是为了保证在多线程并发的情况下保证资源操作的原子性,那么对一个资源的不断的加锁解锁操作也就没有必要了,毕竟加锁解锁的操作也是消耗性能的。因此当偏向锁开启后,如果没有其他线程进行抢锁的操作的话,当前线程t1在完成了锁的操作后,对象的锁也不会自动解锁掉,而是会继续持续一段时间,避免t1再次调用时,又要进行加锁操作,这样也就节省了部分性能。但是当有其他线程来抢锁时,偏向锁就会进行锁升级,变为轻量级锁,之后的锁升级也就和前文说的一样了。


后记

综上所诉,针对前言提出的三个问题,大家是否已经知道答案了呢。如果是,想必你对于synchronized关键字也有了自己的理解了,身为作者我也是有点小骄傲了,哈哈哈哈,也算是给出了一点点的贡献了。最后愿大家能坚持学习,坚持输出。努力做个快乐有梦想的小码农,哈哈哈哈哈。

你可能感兴趣的:(高性能编程原理,java,synchronized)