欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

synchronized凭什么锁得住?

程序员文章站 2022-06-19 17:00:53
相关链接: 《synchronized锁住的是谁?》 我们知道synchronized是重量级锁,我们知道synchronized锁住的是一个对象上的Monitor对象,我们也知道synchronized用于同步代码块时会执行monitorenter和monitorexit等。 上面几个问题仅仅是校 ......

相关链接:

我们知道synchronized是重量级锁,我们知道synchronized锁住的是一个对象上的monitor对象,我们也知道synchronized用于同步代码块时会执行monitorenter和monitorexit等。

上面几个问题仅仅是校招级。

那么synchronized为什么“重”呢?monitor对象从何而来呢?synchronized用于实例方法或者静态方法又是怎么锁住的呢?

在中我们明确了,synchronized锁住的对象,本文讲述synchronized凭什么锁得住。

首先我们需要知道的是在hotspot虚拟机实现中,对象实例在堆内存中结构分为3个部分:对象头、实例数据、对其填充字节。在java中万物皆为对象。就算一个java类被编译称为class二进制文件在被加载到内存时,它仍然会在堆内存中创建一个class对象。这也就解释了,为什么synchronized能对类加锁(因为每个类在堆内存中有一个class对象,对于类synchronized锁的实际上是class对象,下文会继续解释)。

在解释了java中对象实例在hotspot中的内存结构(对象头、实例数据、对其填充字节)后,synchronized锁住的monitor对象就存在于对象头之中。对象头又分为:mark word、指向类的指针、数组长度(数组对象)。

对象头在hotspot虚拟机实现中,分为32位和64位的实现,实际上hotspot源代码实现中的注释已经解释得非常清楚了(openjdk/hotspot/share/oops/markoop.hpp),对象头的mark word位格式在32位机器中是32位长,在64位机器中是64位长(采用 big endian ,低地址存放最高有效字节,即低位在左,高位再右)。

32bit位虚拟机mark word

锁状态

25bit

4bit

1bit

2bit

23bit

2bit

是否是偏向锁

锁标志位

无锁状态

对象的hashcode

分代年龄

0

01

偏向锁

线程id

偏向时间戳

分代年龄

1

01

轻量级锁

指向栈中锁记录的指针

00

重量级锁

指向重量级锁(monitor)的指针

10

gc标记

11

和synchronized相关的就是java在hotspot虚拟机实现中对象头中的mark word。

在以前(jdk5之前),synchronized被称为重量级锁是无可厚非的,但在jdk6后,jvm对其进行了一系列优化,尽量使得synchronized不再那么重。之所以synchronized重,是因为它涉及到了操作系统用户态与核心态的转换,下文再详细解释。这里我们从最轻的偏向锁->轻量级锁->重量级锁的过程,注意他们只能升级加锁的强度,不能降级。

偏向锁

上面提到了jdk6过后优化了synchronized的加锁过程,尽量使得synchronized不再那么重。偏向锁即是如此。

jvm的研究者表明,大多数情况下锁的竞争不是那么激励,在不那么激励的时候如果通过获取monitor来进行同步访问,会造成线程在操作系统用户态和核心态的转换,这会使得系统性能下降。偏向锁表示,当只有一个线程进入同步方法或同步代码块时,并不会直接获取monitor锁,而是先判断对象头中mark word部分的锁标志位是否处于“01”,如果处于“01”,此时再判断线程id是否是本线程id,如果是则直接进入方法进行后续操作;如果不是,此时则通过cas(无锁机制竞争)如果竞争成功,此时将线程id设置为本线程id,如果竞争失败,说明造成了有了较为强烈的锁竞争,偏向锁已不能满足,此时偏向锁晋级为轻量级锁。

轻量级锁

当锁发生竞争时,持有偏向锁的线程会撤销偏向锁,转而晋级为轻量级锁(状态)。轻量级锁的核心是,不让未获取锁的线程进入阻塞状态,因为这会使得线程由用户态转为核心态,这会造成很大的性能损失,而是采用“死循环”的方式不断的获取锁,这种采用“死循环”获取的锁的方式称为——锁自旋。它不会让线程陷入阻塞,但同时仅适用于持有锁时间较短的场景。那么轻量级锁升级为重量级锁的条件就是,自旋等待的时间过长,并且又有了新的线程来竞争。

重量级锁

这种锁,就是地地道道原原本本synchronized的本意了。线程会去抢夺对象上的一个互斥量(这个互斥量就是monitor),每个对象都会有,就算是类也有一个monitor互斥量(因为类在堆内存中有一个class对象)。当一个线程获取到对象的monitor锁时,其余线程会被阻塞挂起,并且由用户态转为核心态。

上文提到在锁的竞争状态晋级为重量级锁时,java对象头中的mark word前30位存储的是monitor对象的指针。monitor对象定义在openjdk/hotspot/share/runtime/objectmonitor.hpp中,在objectmonitor中定义了:计数器、持有monitor的线程、处于wait状态的线程、处于阻塞状态的线程等等。

synchronized无论是普通实例还是同步代码块,它所获取的锁是对象实例中的monitor锁,而对象的monitor又是存在于java对象头的mark work之中,所以可以这么说,synchronized获取的锁在java对象头中。对于普通实例或者静态方法,jvm并没有显示的指令进入临界区,而是在方法上标识了“acc_synchronized”,标识是synchronized同步方法,方法内部都是临界区。而对于同步代码块,则在synchronized代码块开始执行了monitorenter,结束或者抛出异常时执行了monitorexit指令。

synchronized凭借的就是monitor锁住的对象,monitor又是借助于操作系统的mutex lock,之所以它重是因为它被挂起后线程会由用户态转换为内核态,这个转换会带来性能损耗。jdk6开始对其进行了优化,提出了偏向锁和轻量级锁,针对锁竞争较为激烈的场景不会直接去获取monitor对象,减少性能损耗。因此在现如今的synchronized实现中,它的性能劣势也已不再那么明显。

 

 

这是一个能给程序员加buff的公众号 (coderbuff)

synchronized凭什么锁得住?