Java-synchronized实现详解(从Java到汇编)

news/2025/2/12 8:38:34/

synchronized作为java语言中的并发关键词,其在代码中出现的频率相当高频,大多数开发者在涉及到并发场景时,一般都会下意识得选取synchronized。

synchronized在代码中主要有三类用法,根据其用法不同,所获取的锁对象也不同,如下所示:

  • 修饰代码块:这种用法通常叫做同步代码块,获取的锁对象是在synchronized中显式指定的
  • 修饰实例方法:这种用法通常叫做同步方法,获取的锁对象是当前的类对象
  • 修饰静态方法:这种用法通常叫做静态同步方法,获取的锁对象是当前类的类对象

下面我们一起来测试下三种方式下,对象锁的归属及锁升级过程,SynchronizedTestClass类代码如下:

import org.openjdk.jol.info.ClassLayout;public class SynchronizedTestClass {private Object mLock = new Object();public void testSynchronizedBlock(){System.out.println("before get Lock in thread:"+Thread.currentThread().getName()+">>>"+ ClassLayout.parseInstance(mLock).toPrintable());synchronized (mLock) {System.out.println("testSynchronizedBlock start:"+Thread.currentThread().getName());System.out.println("after get Lock in thread:"+Thread.currentThread().getName()+">>>"+ ClassLayout.parseInstance(mLock).toPrintable());try {Thread.sleep(10000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("testSynchronizedBlock end:"+Thread.currentThread().getName());}}public synchronized void testSynchronizedMethod() {System.out.println("after get Lock in thread:"+Thread.currentThread().getName()+">>>"+ ClassLayout.parseInstance(this).toPrintable());System.out.println("testSynchronizedMethod start:"+Thread.currentThread().getName());try {Thread.sleep(10000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("testSynchronizedMethod end:"+Thread.currentThread().getName());}public static synchronized void testSynchronizedStaticMethod() {System.out.println("after get Lock in thread:"+Thread.currentThread().getName()+">>>"+ ClassLayout.parseInstance(SynchronizedTestClass.class).toPrintable());System.out.println("testSynchronizedStaticMethod start:"+Thread.currentThread().getName());try {Thread.sleep(10000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("testSynchronizedStaticMethod end:"+Thread.currentThread().getName());}
}

同步代码块

在main函数编写如下代码,调用SynchronizedTestClass类中包含同步代码块的测试方法,如下所示:

public static void main(String[] args) {SynchronizedTestClass synchronizedTestClass = new SynchronizedTestClass();ExecutorService testSynchronizedBlock = Executors.newCachedThreadPool();testSynchronizedBlock.execute(new Runnable() {@Overridepublic void run() {synchronizedTestClass.testSynchronizedBlock();}});testSynchronizedBlock.execute(new Runnable() {@Overridepublic void run() {synchronizedTestClass.testSynchronizedBlock();}});
}

运行结果如下:

1-4-10-1

从上图可以看出在线程2获取锁前,mLock处于无锁状态,等线程2获取锁后,mLock对象升级为轻量级锁,等线程1获取锁后升级为重量级锁,有同学要问了,你在多线程与锁中不是说了synchronized锁升级有四个吗?你是不是写BUG了,当然没有啊,现在我们来看看偏向锁去哪儿了?

偏向锁

对于不同版本的JDK而言,其针对偏向锁的开关和配置均有所不同,我们可以通过执行java -XX:+PrintFlagsFinal -version | grep BiasedLocking来获取偏向锁相关配置,执行命令输出如下:

1-4-10-2

从上图可以看出在JDK 1.8上,偏向锁默认开启,具有4秒延时,那么我们修改main内容,延时5秒开始执行,看看现象如何,代码如下:

public static void main(String[] args) {try {Thread.sleep(5000);} catch (InterruptedException e) {throw new RuntimeException(e);}SynchronizedTestClass synchronizedTestClass = new SynchronizedTestClass();ExecutorService testSynchronizedBlock = Executors.newCachedThreadPool();testSynchronizedBlock.execute(new Runnable() {@Overridepublic void run() {synchronizedTestClass.testSynchronizedBlock();}});testSynchronizedBlock.execute(new Runnable() {@Overridepublic void run() {synchronizedTestClass.testSynchronizedBlock();}});
}

输出如下:

1-4-10-3

从上图可以看出在延迟5s执行后,mLock锁变成了无锁可偏向状态,结合上面两个示例,我们可以看出,在轻量级锁和偏向锁阶段均有可能直接升级成重量级锁,是否升级依赖于当时的锁竞争关系,据此我们可以得到synchronized锁升级的常见过程,如下图所示:

synchronized

可以看出,我们遇到的两种情况分别对应升级路线1和升级路线4。

同步方法

使用线程池调用SynchronizedTestClass类中的同步方法,代码如下:

public static void main(String[] args) {SynchronizedTestClass synchronizedTestClass = new SynchronizedTestClass();ExecutorService testSynchronizedBlock = Executors.newCachedThreadPool();testSynchronizedBlock.execute(new Runnable() {@Overridepublic void run() {synchronizedTestClass.testSynchronizedMethod();}});testSynchronizedBlock.execute(new Runnable() {@Overridepublic void run() {synchronizedTestClass.testSynchronizedMethod();}});
}

运行结果如下:

1-4-10-4

可以看出,在调用同步方法时,直接升级为重量级锁,同一时刻,有且仅有一个线程在同步方法中执行,其他函数在同步方法入口处阻塞等待。

静态同步方法

使用线程池调用SynchronizedTestClass类中的静态同步方法,代码如下

    public static void main(String[] args) {ExecutorService testSynchronizedBlock = Executors.newCachedThreadPool();testSynchronizedBlock.execute(new Runnable() {@Overridepublic void run() {SynchronizedTestClass.testSynchronizedStaticMethod();}});testSynchronizedBlock.execute(new Runnable() {@Overridepublic void run() {SynchronizedTestClass.testSynchronizedStaticMethod();}});}

运行结果如下:

1-4-10-5

可以看出,在调用静态同步方法时,直接升级为重量级锁,同一时刻,有且仅有一个线程在静态同步方法中执行,其他函数在同步方法入口处阻塞等待。

前面我们看的是多个线程竞争同一个锁对象,那么假设我们有三个线程分别执行这三个函数,又会怎样呢?代码如下:

public static void main(String[] args) {SynchronizedTestClass synchronizedTestClass = new SynchronizedTestClass();ExecutorService testSynchronizedBlock = Executors.newCachedThreadPool();testSynchronizedBlock.execute(new Runnable() {@Overridepublic void run() {SynchronizedTestClass.testSynchronizedStaticMethod();}});testSynchronizedBlock.execute(new Runnable() {@Overridepublic void run() {synchronizedTestClass.testSynchronizedMethod();}});testSynchronizedBlock.execute(new Runnable() {@Overridepublic void run() {synchronizedTestClass.testSynchronizedBlock();}});
}

运行结果:

1-4-10-10

可以看到,3个线程各自运行,互不影响,这也进一步印证了前文所说的锁对象以及MarkWord中标记锁状态的概念。

synchronized实现原理

上面已经学习了synchronized的常见用法,关联的锁对象以及锁升级的过程,接下来我们来看下synchronized实现原理,仍然以上面的SynchronizedTestClass为例,查看其生成的字节码来了解synchronized关键字的实现。

同步代码块

testSynchronizedBlock其所对应的字节码如下图所示:

1-4-10-6

从上图代码和字节码对应关系可以看出,在同步代码块中获取锁时使用monitorenter指令,释放锁时使用monitorexit指令,且会有两个monitorexit,确保在当前线程异常时,锁正常释放,避免其他线程等待死锁。

所以synchronized的同步机制是依赖monitorenter和monitorexit指令实现的,而这两个指令操作的就是mLock对象的monitor锁,monitorenter尝试获取mLock的monitor锁,如果获取成功,则monitor中的计数器+1,同时记录相关线程信息,如果获取失败,则当前线程阻塞。

Monitor锁就是存储在MarkWord中的指向重量级锁的指针所指向的对象,每个对象在构造时都会创建一个Monitor锁,用于监视当前对象的锁状态以及持锁线程信息,

同步方法

testSynchronizedMethod其所对应的字节码如下图所示:

1-4-10-7

可以看到同步方法依赖在函数声明时添加ACC_SYNCHRONIZED标记实现,在函数被ACC_SYNCHRONIZED修饰时,调用该函数会申请对象的Monitor锁,申请成功则进入函数,申请失败则阻塞当前线程。

静态同步方法

testSynchronizedStaticMethod其所对应的字节码如下图所示:

1-4-10-8

和同步方法相同,同步静态方法也是在函数声明部分添加了ACC_SYNCHRONIZED标记,与同步方法不同的是,此时申请的是该类的类对象的Monitor锁。


扩展

上文中针对synchronized的java使用以及字节码做了说明,我们可以看出synchronized是依赖显式的monitorenter,monitorexit指令和ACC_SYNCHRONIZED实现,但是字节码并不是最靠近机器的一层,相对字节码,汇编又是怎么处理synchronized相关的字节码指令的呢?

我们可以通过获取java代码的汇编代码来查看,查看Java类的汇编代码需要依赖hsdis工具,该工具可以从https://chriswhocodes.com/hsdis/下载(科学上网),下载完成后,在Intellij Idea中配置Main类的编译参数如下图所示:

1-4-10-11

其中vm options详细参数如下:

-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp -XX:CompileCommand=compileonly,*SynchronizedTestClass.testSynchronizedBlock -XX:CompileCommand=compileonly,*SynchronizedTestClass.testSynchronizedMethod -XX:+LogCompilation -XX:LogFile=/Volumes/Storage/hotspot.log

其中“compileOnly,”后面跟的是你要抓取的函数名称,格式为:*类名.函数名,LogFile=后指向的是存储汇编代码的文件。

环境变量配置如下:

LIBRARY_PATH=/Volumes/Storage/hsdis

这里的写法是:hsdis存储路径+/hsdis

随后再次运行Main.main即可看到相关汇编代码输出在运行窗口,通过分析运行窗口输出的内容,我们可以看到如下截图:

1-4-10-9

可以看出在运行时调用SynchronizedTestClass::testSynchronizedMethod时,进入synchronized需要执行lock cmpxchg以确保多线程安全,故synchronized的汇编实现为lock cmpxchg指令。

参考链接

https://juejin.cn/post/6844904038580879367

https://segmentfault.com/a/1190000014315651?u_atoken=1de4c0b1-3bbd-4f24-a1ab-a997765e1e1a&u_asession=01_GMzG_R8y1NzmMkw-_FjCBDyAkKZZeqX02Aj0fLUi3NY0u-oBsC7Dy3mYDlaYjVfX0KNBwm7Lovlpxjd_P_q4JsKWYrT3W_NKPr8w6oU7K_YrukAhz0rn57pEYiZgVtjUe3R9QHfzEvknA4dzJmVTGBkFo3NEHBv0PZUm6pbxQU&u_asig=05zOyEpzkQtID72ACuBpeb8Glaue6lNWy0NQylEbR2Xc_7kNzOQ26VSExsqtnzBe0Xx0y_nHbPn6RxgW3P4ycjRX6ouZzhKXbyHd2wyK1BU-yIj5LEU571xoQb6N65-U8YiNzYYvkK1yxbcelpg93XN_0VgtmNHNEhLa9ouEeFbkf9JS7q8ZD7Xtz2Ly-b0kmuyAKRFSVJkkdwVUnyHAIJzbnmdT2ThIIvmF2G_c1IyFPFdBjB2ky9pbFgAVRR13T0H_8T8uYGNepqxdb-gLe1IO3h9VXwMyh6PgyDIVSG1W8yga-85GxBTGJYmsELhoUH8mRFOQ3P7irc0oGyG8T1Ftbng4zz7ikYTVlpim2ptLJ8hIoW81rm0R3x30M6VpJOmWspDxyAEEo4kbsryBKb9Q&u_aref=Vatq7Ew4O%2BKsO9AD17eeU57gwco%3D


http://www.ppmy.cn/news/55798.html

相关文章

华为2023暑期笔试(2-2)——最近最少使用(LRU, Least recently used)缓存算法

目录 题目内容解答要求(解答要求限制了只能使用LRU)输入描述样例思路代码(还是超时了,等以后想到有什么方法可以优化再来改) 题目内容 你是一名网络工程师,你正在为一家云计算公司开发一个虚拟机管理系统。…

学系统集成项目管理工程师(中项)系列13b_人力资源管理(下)

1. 项目团队建设 1.1. 塔克曼(Tuckman)阶梯理论 1.2. 理论基础 1.2.1. 激励理论 1.2.1.1. 马斯洛需要层次理论 1.2.1.1.1. 生理需要 1.2.1.1.2. 安全需要 1.2.1.1.3. 社会交往的需要 1.2.1.1.4. 自尊的需要 1.2.1.1.5. 自我实现的需要 1.2.1.2. 赫茨伯格的双因素理论…

PYQT5学习笔记05——QObject父子对象API以及案例

一、父子对象API 我们在这里简单演示一下父子对象API的具体用法以及代码实现,父子对象API有五个,分别是setParent、parent、children、findChild、findChildren,接下来对每一个API都具体演示一下。 1、setParent(parent)和parent() setParen…

ldif 数据转成正确的组织结构再探

上次文章最后有说到按照之前的思路来转化组织结构是有坑的,我们现在还只是对接 AD域,ldap 协议的其他产品在细节上还会有些许不同 我们是不能直接粗暴的认为 cn 就是对应标识一个用户, cn 是 common name 的意思,他也可以表示我们…

设计模式之原型模式(深拷贝浅拷贝)

目录 1、什么是原型模式 2、前置知识(深拷贝&浅拷贝) 2.1 浅拷贝 2.2 深拷贝 3、代码实现 3.1 通过Object中的clone方法实现浅拷贝 3.2 通过对象流来实现深拷贝 4、原型模式总结 4.1 优缺点 4.2 使用场景 4.3 对比直接new对象有何不同 1、…

【信息安全案例】——系统软件安全(学习笔记)

📖 前言:操作系统是管理系统资源、控制程序执行、提供良好人机界面和各种服务的一种系统软件,是连接计算机硬件与上层软件和用户之间的桥梁。因此,操作系统是其他系统软件、应用软件运行的基础,操作系统的安全性对于保…

Netty

Netty 背景知识 网络编程基础 TCP协议 TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议 详情看TCP/IP面试题 为什么要有“连接”?什么是”连接“ Byte Stream Oriented vs Message Oriented Socket API Socket() Bind() Listen() Accept() Read() …

「SQL面试题库」 No_50 产品销售分析 II

🍅 1、专栏介绍 「SQL面试题库」是由 不是西红柿 发起,全员免费参与的SQL学习活动。我每天发布1道SQL面试真题,从简单到困难,涵盖所有SQL知识点,我敢保证只要做完这100道题,不仅能轻松搞定面试&#xff0…