【Java技术指南】「原理剖析」synchronized工作原理分析

简介: 【Java技术指南】「原理剖析」synchronized工作原理分析

一、大致介绍


1、用过synchronized的童鞋都知道这个关键字是Java中用于解决并发情况下数据的同步访问; 2、保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性; 3、总的来说,其作用有三个特性:互斥性(确保线程互斥的访问同步代码)、可见性(保证共享变量的修改能够及时可见)、有序性(有效解决重排序问题); 4、本章节就此和大家分享一下synchronized底层语义原理;



二、特性


2.1 互斥性


1、互斥性,可以认为独享的意思,每次只允许一个操作者拥有共享资源;


2、被synchronized修饰的代码块、实例方法、静态方法,多线程并发访问时,只能有一个线程获取到锁,其它线程都处于阻塞等待, 但在此期间,这些线程仍然可以访问其它非synchronized修饰的方法;



2.2 可见性


1、可见性,就是每次线程的到来,都能访问到最新的值;


2、因为在互斥性的基础上,由于每次仅有一个线程执行临界区的代码,因此其修改的任何变量值对于稍后执行该临界区的线程来说是可见的;


3、再多说一句,因为互斥性的存在,也保证了临界区变量修改的原子性,而volatile仅仅只能保证变量修改的可见性,并不能保证原子性;



2.3 有序性


1、有序性,就是按照顺序来执行;


2、同样因为在互斥性的基础上,代码块也好,实例方法或静态方法也好,一旦被synchronized后,各个线程相互竞争,反正每次只能有一个线程执行;


3、打个比方,举例静态方法,TestSynchronized.java 中有个静态 synchronized static test(){ i++, j++} 方法, 并且代码块被synchronized修饰,让N个线程都去调用这个方法,最后会发现每次i和j的输出值都是一样的。 i++和j++要么一起执行完,要么都不执行,不会出现先i++后,执行了其他代码,过一会再执行j++的情况。




三、反编译查看字节码


3.1、反编译同步代码块


1、通过 javap -verbose 反编译代码块(反编译的汇编代码就不粘贴出来了),最后会发现被反编译的代码块的前后被monitorenter、monitorexit一对指令包夹着;


2、关于这两条指令的作用,我们直接参考JVM规范中描述:

monitorenter :
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.
monitorexit: 
• The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
• The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
复制代码


3、monitorenter指令JVM规范翻译: 每个对象有打自娘胎出来就自带一个内置监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下: • 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。 • 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1. • 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。


4、monitorexit指令JVM规范翻译: • 执行monitorexit的线程必须是objectref所对应的monitor的所有者。 • 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。



3.2、反编译同步方法


1、通过 javap -verbose 反编译同步方法(反编译的汇编代码就不粘贴出来了),最后会发现被反编译的同步方法附近有一个ACC_SYNCHRONIZED标示符;


2、关于ACC_SYNCHRONIZED指令的作用,我们直接参考JVM规范中描述:

ACC_SYNCHRONIZED:
Method-level synchronization is performed implicitly, as part of method invocation and return. 
A synchronized method is distinguished in the run-time constant pool's methodinfo structure by the ACCSYNCHRONIZED flag, which is checked by the method invocation instructions. 
When invoking a method for which ACC_SYNCHRONIZED is set, the executing thread enters a monitor, invokes the method itself, and exits the monitor whether the method invocation completes normally or abruptly. 
During the time the executing thread owns the monitor, no other thread may enter it. 
If an exception is thrown during invocation of the synchronized method and the synchronized method does not handle the exception, the monitor for the method is automatically exited before the exception is rethrown out of the synchronized method.
复制代码


3、ACC_SYNCHRONIZED指令JVM规范翻译: • 方法级的同步是隐式的。同步方法的常量池中会有一个ACC_SYNCHRONIZED标志。 • 当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED, • 如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。 • 这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。 • 值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。



3.3、反编译小结


无论是monitorenter、 monitorexit,或者是ACC_SYNCHRONIZED,其都是基于Monitor实现的,因此接下来有必要了解下Monitor是什么东西,在了解Monitor的时候,我们还有必要了解下java对象头。





四、Java对象头、Monitor


4.1、Java对象头


1、在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和填充数据,这个在下面图3、图4、图5都可以看到布局图,例如“实例对象(对象锁)”;


2、而Java对象头里面包含Mark Word( 存储对象的hashCode、锁信息或分代年龄或GC标志等信息 )、Klass Pointer( 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例 )。


3、至于Mark Word里面具体包含了哪些信息,结构是怎么样的,请看下面图1;



4.2、Java对象头作用


1、Java头对象,它实现synchronized的锁对象的基础;


2、特别是Java对象头存储的锁信息,对synchronized的优化起到举足轻重的作用,其中轻量级锁、偏向锁是jdk6对synchronized锁进行优化后新增加的;


3、至此,如果对Java对象头还不理解的话,简单粗暴的讲,我们要依赖对象头Mark Word中的锁信息判断来决定如何优化synchronized;


4、如果还不理解的话,后面还会讲解到利用Java对象头的哪些信息是怎么做到synchronized锁优化的;




4.3、什么是Monitor?


Monitor其实是一种同步工具,也可以说是一种同步机制,它通常被描述为一个对象;


1、对象的所有方法都被“互斥”的执行。好比一个Monitor只有一个运行“许可”,任一个线程进入任何一个方法都需要获得这个“许可”,离开时把许可归还。


2、通常提供singal机制:允许正持有“许可”的线程暂时放弃“许可”,等待某个谓词成真(条件变量),而条件成立后,当前进程可以“通知”正在等待这个条件变量的线程,让他可以重新去获得运行许可。




4.4、Monitor粗俗理解


1、Java对象是天生的Monitor,每一个对象自打娘胎里出来,就带了一把看不见的锁,通常我们叫“内部锁”,或者“Monitor锁”,或者“Intrinsic lock”。


2、Java对象与这把内置锁紧密关联着,每个对象都存在着一个 monitor 与之关联,当锁升级为重量级锁时,监视器Monitor这把内置锁用来监视这些线程进入特殊的房间的,他的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码。


3、在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,数据结构位于HotSpot虚拟机源码ObjectMonitor.hpp文件(hg.openjdk.java.net/jdk8/jdk8/h…




4.5、Java对象头、Monitor小结


1、讲到这里,大家对synchronized的原理所涉及到的一些知识点有了大概的了解,那他们究竟是如何互相作用将synchronized玩转的呢?


2、在jdk6之前,起主导作用的仍然是重量级锁,但是随着jdk6的问世,synchronized添加偏向锁、轻量级锁,优化了synchronized锁的获取方式;


3、对于“无锁->偏向锁->轻量级锁”的转变,我们主要看线程的栈帧、Java对象头,源码在jdk8的synchronizer.cpp中(hg.openjdk.java.net/jdk8/jdk8/h…


4、对于“轻量级锁->重量级锁”,我们主要看Monitor,源码在jdk8的objectMonitor.cpp中(hg.openjdk.java.net/jdk8/jdk8/h…




五、锁的类型


无锁->偏向锁->轻量级锁->重量级锁,它会随着竞争情况逐渐升级。锁可以升级但不能降级,目的是为了提高获得锁和释放锁的效率。
复制代码

5.1、偏向锁


1、引入原由:大多数情况下,锁的竞争关系都是单一的,锁由同一个线程多次获取,为了降低获取锁的代价才引入了偏向锁,减少了一些不必要的CAS操作;


2、偏向锁加锁流程:

线程请求获取锁 ----→ 目前锁状态为01
             ¦
             ¦
             ↓           N
           是否偏向锁? ------------→ 利用 CAS 操作替换 Mark Word 的线程ID
             ¦             ↑  ¦         ¦
             ¦Y            ¦  ¦         ¦N
             ↓          N   ¦ ¦         ↓           Y
  ¦--→  检查Mark Word是否记录当前线程ID? -----¦   ¦Y     检测对象锁的锁偏向是否偏向? ----------→ 尝试CAS替换线程ID,
  ¦          ¦                  ¦         ¦              此时会引发竞争,升级为轻量级锁
  ¦          ¦Y                 ¦         ¦
  ¦          ↓                  ¦         ¦
  ¦   获得偏向锁并将锁的偏向改为1  ←-------------¦         ¦
  ¦          ¦                        ¦
  ¦          ¦                        ¦N
  ¦          ↓                        ¦
  ¦       执行同步代码块                       ¦
  ¦                                   ¦
  ¦                                 ¦
  ¦--------------------------------------------------------------------¦                      
复制代码



5.2、轻量级锁


1、引入原由:在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁,取而代之的是在monitorenter和monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。 当存在锁竞争的情况下,执行CAS指令失败的线程将调用操作系统互斥锁进入到阻塞状态,当锁被释放的时候被唤醒。


2、轻量级锁加锁流程:

升级为轻量级锁 ----→ 原持有偏向锁的线程在线程栈帧分配锁记录Lock Record       ¦------→  当前线程栈帧分配锁记录
                     ¦                  ¦       ¦
                     ¦                  ¦       ¦
                     ↓                  ¦       ↓
        拷贝对象锁中的对象头的Mark Word到Lock Record的Header中      ¦ 拷贝对象头的Mark Word到锁记录中        再尝试
                     ¦                  ¦       ¦           ¦------------¦
                     ¦                  ¦       ¦           ¦     ¦
                     ↓                当前线程   ¦        ↓           ↓     ¦
          原持有偏向锁的线程获得轻量级锁,锁标志位改为00  --------------¦ CAS操作,将对象头的锁记录指针指向当前线程锁记录? ----→  自旋计数N次
                     ¦                           ¦                    ¦
                     ¦                           ¦Y                    ¦
                     ↓                           ↓                    ↓
                原持有偏向锁的线程                  当前线程获得轻量级锁             N次CAS还是失败的话,
                     ¦                           ¦                  则升级为重量级锁
                     ¦                           ¦
                     ↓                           ↓
                 安全点执行同步代码块                   执行同步代码块
                     ¦                           ¦
                     ¦                           ¦
                     ¦                           ↓
                     ¦-------------------------------------------→   开始轻量级锁解锁
复制代码



5.3、重量级锁


1、重量级锁就不用讲了,一直沿用至今,主要利用内置锁,内置锁的本质是依赖操作系统,因此内置锁对各个线程的阻塞是由操作系统完成(在Linxu下通过pthread_mutex_lock函数);

2、重量级锁加锁流程:

当前线程                  Y
升级为重量级锁 ----→ 原有线程获得重量级锁,  --------------→  内置锁_owner==null? ------→  当前线程获取到锁
        Mark Word指向内置锁Monitor指针       ↑    ¦
                              ¦    ¦N
                              ¦    ↓             Y
                              ¦ _owner==self当前线程?  -------→  重入锁获取到锁
                              ¦     ¦
                              ¦     ¦N
                              ¦---------¦
                              自旋  ¦
                                  ¦
                                  ¦
                               尝试自旋仍未获得锁
                                  ¦
                                  ¦
                                  ↓
                    将线程封装成ObjectWaiter插入_cxq队列中,CAS将_cxq指向该ObjectWaiter
                                  ¦
                                  ¦
                                  ↓
                                 part挂起
复制代码


5.4、锁分类小结


1、至此,各个锁的加锁操作已经差不多了解了一番,此刻我们应该改正monitorenter指令就是获取对象重量级锁的错误认识,很显然,优化之后,锁的获取判断次序是偏向锁->轻量级锁->重量级锁。


2、偏向锁重要的两个方法fast_enter、fast_exit,jdk源码路径(hg.openjdk.java.net/jdk8/jdk8/h…);


3、轻量级锁重要的两个方法slow_enter、slow_exit,jdk源码路径(hg.openjdk.java.net/jdk8/jdk8/h…);


4、重量级锁重要的三个方法enter、EnterI、exit,jdk源码路径(hg.openjdk.java.net/jdk8/jdk8/h…);




六、synchronized锁总结图片



image.png

image.png


相关文章
|
监控 Cloud Native Java
Quarkus 云原生Java框架技术详解与实践指南
本文档全面介绍 Quarkus 框架的核心概念、架构特性和实践应用。作为新一代的云原生 Java 框架,Quarkus 旨在为 OpenJDK HotSpot 和 GraalVM 量身定制,显著提升 Java 在容器化环境中的运行效率。本文将深入探讨其响应式编程模型、原生编译能力、扩展机制以及与微服务架构的深度集成,帮助开发者构建高效、轻量的云原生应用。
1142 44
|
安全 Java API
Java Web 在线商城项目最新技术实操指南帮助开发者高效完成商城项目开发
本项目基于Spring Boot 3.2与Vue 3构建现代化在线商城,涵盖技术选型、核心功能实现、安全控制与容器化部署,助开发者掌握最新Java Web全栈开发实践。
936 1
|
安全 Java 编译器
new出来的对象,不一定在堆上?聊聊Java虚拟机的优化技术:逃逸分析
逃逸分析是一种静态程序分析技术,用于判断对象的可见性与生命周期。它帮助即时编译器优化内存使用、降低同步开销。根据对象是否逃逸出方法或线程,分析结果分为未逃逸、方法逃逸和线程逃逸三种。基于分析结果,编译器可进行同步锁消除、标量替换和栈上分配等优化,从而提升程序性能。尽管逃逸分析计算复杂度较高,但其在热点代码中的应用为Java虚拟机带来了显著的优化效果。
426 4
|
Java API Maven
2025 Java 零基础到实战最新技术实操全攻略与学习指南
本教程涵盖Java从零基础到实战的全流程,基于2025年最新技术栈,包括JDK 21、IntelliJ IDEA 2025.1、Spring Boot 3.x、Maven 4及Docker容器化部署,帮助开发者快速掌握现代Java开发技能。
1939 1
|
12月前
|
存储 Java Go
【Java】(3)8种基本数据类型的分析、数据类型转换规则、转义字符的列举
牢记类型转换规则在脑海中将编译和运行两个阶段分开,这是两个不同的阶段,不要弄混!
447 2
|
12月前
|
Java Go 开发工具
【Java】(9)抽象类、接口、内部的运用与作用分析,枚举类型的使用
抽象类必须使用abstract修饰符来修饰,抽象方法也必须使用abstract修饰符来修饰,抽象方法不能有方法体。抽象类不能被实例化,无法使用new关键字来调用抽象类的构造器创建抽象类的实例。抽象类可以包含成员变量、方法(普通方法和抽象方法都可以)、构造器、初始化块、内部类(接 口、枚举)5种成分。抽象类的构造器不能用于创建实例,主要是用于被其子类调用。抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类abstract static不能同时修饰一个方法。
425 1
|
数据采集 存储 弹性计算
高并发Java爬虫的瓶颈分析与动态线程优化方案
高并发Java爬虫的瓶颈分析与动态线程优化方案
|
Java 测试技术 API
2025 年 Java 开发者必知的最新技术实操指南全览
本指南涵盖Java 21+核心实操,详解虚拟线程、Spring Boot 3.3+GraalVM、Jakarta EE 10+MicroProfile 6微服务开发,并提供现代Java开发最佳实践,助力开发者高效构建高性能应用。
1599 5
|
安全 Cloud Native Java
Java 模块化系统(JPMS)技术详解与实践指南
本文档全面介绍 Java 平台模块系统(JPMS)的核心概念、架构设计和实践应用。作为 Java 9 引入的最重要特性之一,JPMS 为 Java 应用程序提供了强大的模块化支持,解决了长期存在的 JAR 地狱问题,并改善了应用的安全性和可维护性。本文将深入探讨模块声明、模块路径、访问控制、服务绑定等核心机制,帮助开发者构建更加健壮和可维护的 Java 应用。
1138 0
|
算法 Java 程序员
论文翻译 | 【深入挖掘Java技术】「底层原理专题」深入分析一下并发编程之父Doug Lea的纽约州立大学的ForkJoin框架的本质和原理
本文深入探讨了一个Java框架的设计、实现及其性能。该框架遵循并行编程的理念,通过递归方式将问题分解为多个子任务,并利用工作窃取技术进行并行处理。所有子任务完成后,其结果被整合以形成完整的并行程序。 在总体设计上,该框架借鉴了Cilk工作窃取框架的核心理念。其核心技术主要聚焦于高效的任务队列构建和管理,以及工作线程的管理。经过实际性能测试,我们发现大多数程序的并行加速效果显著,但仍有优化空间,未来可能需要进一步研究改进方案。
379 3
论文翻译 | 【深入挖掘Java技术】「底层原理专题」深入分析一下并发编程之父Doug Lea的纽约州立大学的ForkJoin框架的本质和原理