程序员求职经验分享与学习资料整理平台

网站首页 > 文章精选 正文

你真正了解synchronized关键字吗?

balukai 2025-07-10 13:09:33 文章精选 3 ℃

在学习synchronized关键字底层实现原理之前,我们先了解下Java对象在内存中是如何存储的,即它在内存中的存储布局是什么样的呢,看下图:

其中实例变量主要用来存放类的属性信息,包括父类的属性信息;填充数据不是必须存在的,仅仅是为了字节对齐,符合JVM的规范,了解即可。

Java对象的对象头就是我们今天的主角,Java对象可分为普通对象和数组对象,不同对象的对象头各个组成部分是什么样的呢,还是用图说话(以32位JVM为例):

我们重点看看Mark Word的这4个字节(64位JVM则为8个字节)是如何使用的呢?

Klass Word的32位-4字节空间(64位JVM则为64位-8字节空间)主要用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。这个空间可以通过+UseCompressedOops开启指针压缩来节约空间;

Array Length的32位-4字节空间(64位JVM则为64位-8字节空间)用于存储数组长度。

当一个对象锁被升级为重量级锁之后,一个非常重要的对象Monitor就出现了,如上图
ptr_to_heavyweight_monitor这个指针指向的地方即为Monitor对象的地址,我们也顺便聊一下这个对象。在JVM中每个Java对象都自动关联一个Monitor对象,在Hotspot虚拟机的实现中,
Monitor是由ObjectMonitor实现的,见下图所示:

Monitor对象有两个队列,一个叫等待队列_WaitSet,一个叫阻塞队列_EntryList,等待队列存放的是状态为wait的线程,这些线程曾经都获取过锁,只是由于等待某些其他条件的发生或者I/O事件,代码里面调用了锁对象.wait()方法,从而释放了对象锁,此时这些线程都会进入这个等待队列;当多个线程同时进入synchronized代码同步块(也叫锁临界区)时,未获取到对象锁的线程会进入_EntryList队列,而获取到对象锁的线程在存入_owner区域,直到释放锁或者调用锁.wait()方法,_owner置为空;

从字节码的角度看synchronized关键字,其实是编译器通过加入monitorenter和mnitorexit关键字来识别的,当代码执行到monitorenter处时,则将锁对象的MarkWord设置为Monitor对象的地址,当执行到monitorexit处时,则将锁对象的MarkWord重置,并唤醒Monitor对象的_EntryList队列里面的线程,通过CPU的调度机制,和_EntryList队列里面的线程一起,重新竞争锁,如下图所示:

至此,synchronized关键字的底层实现原理也聊得差不多了,那你了解synchronized的锁升级过程吗?

其实,无锁、偏向锁、自旋锁、轻量级锁、重量级锁是在jdk1.6及以后的版本中引入的,锁升级功能主要依赖于对象头 Mark Word 中的锁标志位和是否偏向锁标志位,synchronized 同步锁就是从偏向锁开始的,随着竞争越来越激烈,偏向锁升级到轻量级锁,最终升级到重量级锁。我大体说一下锁升级的过程,更详细的内容,大家自行google吧。

偏向锁的出现,其实是为了优化同一个线程重复获取相同锁资源而导致用户态和内核态频繁切换的低效场景而设计的。例如当一个单线程操作一个线程安全集合时,该线程每次都需要获取和释放锁从而导致上下文来回切换,而偏向锁的作用就是,当一个线程再次访问这个同步代码或方法时,它只需先判断一下锁对象头的MarkWord里是否有偏向锁指向它的 ID,而无需再进入 Monitor 去竞争锁了。

锁升级的过程简述如下:

1)当对象被当做同步锁(例如Object对象),线程A抢到了锁时,lock标志位是 01,biased_lock标志位为 1,并且记录抢到锁的线程A的ID,表示进入偏向锁状态;

2)此时线程B也来竞争Object锁,此时锁标识位是01,偏向锁状态为1,但MarkWord里面的ThreadID不是自己,则通过CAS的方式来获取锁,如果获取成功,则直接将MarkWord中的线程ID更新为线程B的ID;

3)如果获取锁失败,偏向锁就会被撤销(偏向锁的撤销需要等待全局安全点,暂停持有该锁的线程,同时检查该线程是否还在执行该方法,如果是,则升级锁,反之则被其它线程抢占);

3)偏向锁被撤销后,会被升级为轻量级锁,此时偏向锁标志位变为0,锁标志位变为00,Thread A获得轻量级锁,Thread B继续通过CAS的方式获取轻量级锁,如果获取成功,则Thread B持有轻量级锁;

4-1)若Thread B获取轻量级锁失败,则继续进入自旋状态,如果自旋一定次数,还是没有获取到锁,则继续升级;

4-2)若此时Thread C也来竞争锁,先判断Object锁状态位,为轻量级锁,则直接进入自旋状态,自旋一定状态,若没有获取到锁,则升级锁;

5-1)将轻量级锁升级为重量级锁,锁标识位变为01,偏向锁标识位为0,Thread B进入Block状态,进入到Monitor对象的_EntryList队列;

5-2)若此时Thread-D也来竞争锁,先判断Object锁状态位,为重量级锁,则当前线程直接挂起,进入到Monitor对象的_EntryList队列,等待锁释放,CPU的调度,竞争下一次的锁;

以上过程大家可以结合偏向锁&锁标志位的状态一起来看,如下图红框处:


在锁升级的过程中,JVM也提供了一些参数供我们配置进行锁的优化,比如-XX:UseBiasedLocking //关闭偏向锁(默认打开),或-XX:+UseHeavyMonitors //设置重量级锁,我们可以根据具体的业务场景来配置不同的参数。

至此,synchronized关键字的底层实现原理和锁升级的过程大致和大家介绍完了,给大家留个思考题,除了本文介绍的锁升级过程和各种调优参数,你还能想出哪些锁优化机制,欢迎大家在评论区留言。

最近发表
标签列表