Java源码分析系列笔记-2.Synchronized
1. 是什么
Java 中悲观锁的一种实现,相比于 volatile 是重量级锁,可以保证原子性、有序性、可见性
重量级
会引起上下文切换(会造成线程阻塞)原子性
synchronized 方法、synchronized 代码块被视作原子的有序性
线程 A 对于锁 X 的释放发生于线程 B 对于锁 X 的申请之前。
也就是说线程 A 在释放锁之前的所有写操作造成的更新,之后线程 B 在申请锁之后的读操作都可以看到这些更新结果可见性
synchronized 方法或代码块里修改的共享变量,在退出临界区时会写回主内存
2. 什么时候使用
2.1. 多线程访问共享资源时的并发问题
当我们进行多线程开发的时候,需要在多个线程之间进行通信,而通信一般都是通过读写共享变量实现的,如果操作的顺序不当就会出现异常的结果。
举个例子,如下一段程序
public class MultiThread
{
private static int val = 0;
public static void main(String[] args) throws InterruptedException
{
Thread thread1 = new Thread(()->{
for (int i = 0; i < 100000; i++)
{
val++;
}
});
Thread thread2 = new Thread(()->{
for (int i = 0; i < 100000; i++)
{
val--;
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(val);
}
}
thread1 对 val 执行 100000 次加操作,而 thread2 对 val 执行 100000 此减操作,最终的结果应该是 0,但实际得出的结果却是不确定的。
2.1.1. 究其原因
假设这两个线程为 thread1 和 thread2,操作如下:
- thread1
第1步:thread1读取内存中的val到工作内存中,值为0
第2步:thread1对val+1,写回工作内存,此时工作内存中的值为1
第3步:thread1失去cpu
第8步:thread1把工作内存中的1写回主内存 //此时主内存中的值为1!!!
- thread2
第4步:thread2读取内存中的val到工作内存中,值为0
第5步:thread2对val-1,写回工作内存
第6步:thread2把工作内存中的值写回主内存 //此时主内存中的值为-1
第7步:thread2失去cpu
由上面的步骤可以看出最后内存中的 val 为-1,但是正确的结果应该是 0 才对。
2.1.2. 解决的方法
也很简单,就是加锁,如下使用了 synchronized 代码块
public class MultiThread
{
private static int val = 0;
public static void main(String[] args) throws InterruptedException
{
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 100000; i++)
{
synchronized (MultiThread.class)
{
val++;
}
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 100000; i++)
{
synchronized (MultiThread.class)
{
val--;
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(val);
}
}
3. 如何使用
Synchronize 有三种用法
3.1. 修饰 static 方法。使用的锁是当前类对象
public class SychronizedTest1
{
private static StringBuilder stringBuilder = new StringBuilder();
public static void main(String[] args) throws InterruptedException
{
Thread addThread = new Thread(() -> {
for (int j = 0; j < 5000; j++)
{
append("aaaa");
}
});
Thread decrThread = new Thread(() -> {
for (int j = 0; j < 5000; j++)
{
append("aaaa");
}
});
addThread.start();
decrThread.start();
addThread.join();
decrThread.join();
String str = stringBuilder.toString();
System.out.println(str);
System.out.println(str.length());
System.out.println(str.contains("a"));
System.out.println(str.length() == 5000 * 2 * 4);//true
}
private synchronized static void append(String val)
{
stringBuilder.append(val);
}
}
3.2. 修饰普通方法。使用的锁是当前实例对象
public class SychronizedTest2
{
private static StringBuilder stringBuilder = new StringBuilder();
public static void main(String[] args) throws InterruptedException
{
SychronizedTest2 sychronizedTest2 = new SychronizedTest2();
Thread addThread = new Thread(() -> {
for (int j = 0; j < 5000; j++)
{
sychronizedTest2.append("aaaa");
}
});
Thread decrThread = new Thread(() -> {
for (int j = 0; j < 5000; j++)
{
sychronizedTest2.append("aaaa");
}
});
addThread.start();
decrThread.start();
addThread.join();
decrThread.join();
String str = stringBuilder.toString();
System.out.println(str);
System.out.println(str.length());
System.out.println(str.contains("a"));
System.out.println(str.length() == 5000 * 2 * 4);//true
}
private synchronized void append(String val)
{
stringBuilder.append(val);
}
}
因为使用的是当前实例对象,如果创建两个实例对象,那么肯定是线程不安全了,如下:
public class SychronizedTest2
{
private static StringBuilder stringBuilder = new StringBuilder();
public static void main(String[] args) throws InterruptedException
{
SychronizedTest2 sychronizedTest2 = new SychronizedTest2();
SychronizedTest2 sychronizedTest3 = new SychronizedTest2();
Thread addThread = new Thread(() -> {
for (int j = 0; j < 5000; j++)
{
sychronizedTest2.append("aaaa");
}
});
Thread decrThread = new Thread(() -> {
for (int j = 0; j < 5000; j++)
{
sychronizedTest3.append("aaaa");
}
});
addThread.start();
decrThread.start();
addThread.join();
decrThread.join();
String str = stringBuilder.toString();
System.out.println(str);
System.out.println(str.length());
System.out.println(str.contains("a"));
System.out.println(str.length() == 5000 * 2 * 4);//false
}
private synchronized void append(String val)
{
stringBuilder.append(val);
}
}
3.3. 修饰代码块。使用的锁是()里指定的对象
public class SychronizedTest3
{
private static StringBuilder stringBuilder = new StringBuilder();
public static void main(String[] args) throws InterruptedException
{
Thread addThread = new Thread(() -> {
for (int j = 0; j < 5000; j++)
{
append("aaaa");
}
});
Thread decrThread = new Thread(() -> {
for (int j = 0; j < 5000; j++)
{
append("aaaa");
}
});
addThread.start();
decrThread.start();
addThread.join();
decrThread.join();
String str = stringBuilder.toString();
System.out.println(str);
System.out.println(str.length());
System.out.println(str.contains("a"));
System.out.println(str.length() == 5000 * 2 * 4);//true
}
private static void append(String val)
{
synchronized (SychronizedTest3.class)
{
stringBuilder.append(val);
}
}
}
4. sychronized 代码块原理分析
4.1. 字节码实验
在 Idea 中运行下面的代码,并且使用 show byte code 插件查看字节码
public class MultiThread
{
private static int val = 0;
public static void main(String[] args) throws InterruptedException
{
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 100000; i++)
{
synchronized (MultiThread.class)
{
val++;
}
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 100000; i++)
{
synchronized (MultiThread.class)
{
val--;
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(val);
}
}

- 字节码如下:
![]()
我们可以看到,18-21 的代码中对应的字节码有 MONITORENTER 和 MONITOREXIT 指令。
即执行同步代码块之前首先要执行 monitorenter,执行同步代码块之后要执行 monitorexit。
在 jvm 的指令手册中,MONITORENTER 表示进入并获取对象监视器,而 MONITOREXIT 表示释放并退出对象监视器,如下图:
![]()
4.1.1. monitor 是个啥玩意
每个对象都可以看作是一个 monitor。
当这个对象作为 monitor 使用时,同一时间只能由一个线程持有。所谓持有其实就是做个标记,这个标记做在 java 对象头里面
4.1.1.1. JVM 对象组成
4.2. 汇编代码实验
4.2.1. 下载编译 hsdis-amd64.dll
参考How to build hsdis-amd64.dll and hsdis-i386.dll on Windows或者hsdis-amd64.7z
4.3. 放入 JRE bin 目录下

4.3.1. 对比实验
- 没有 sychronized
public class TestSynchronized
{
private static int i = 0;
public static void main(String[] args)
{
test();
}
private static void test()
{
i++;
}
}
- 有 sychronized
public class TestSynchronized
{
private static int i = 0;
public static void main(String[] args)
{
test();
}
private static void test()
{
synchronized (TestSynchronized.class)
{
i++;
}
}
}
4.3.2. 加上 jvm 参数运行
-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:-Inline -XX:CompileCommand=print,*TestSynchronized.test
使用 IDEA 的话如下图:

4.3.3. 输出结果对比
- 加 synchronized.txt
- 没加 synchronized.txt
使用 BeyondCompare 对比发现加 synchronized 的多了 lock 和 monitorenter 等指令,如下:
![]()
![]()
4.4. 原子性
从汇编代码可以看出 monitorenter 与 monitorexit 包裹了如下代码:
0x00000000033254d5: mov 0x68(%rax),%esi ;*getstatic i //从内存中读取val的值到寄存器中
; - com.zsk.test.TestSynchronized::test@5 (line 15)
0x00000000033254d8: inc %esi //执行val++
0x00000000033254da: mov %esi,0x68(%rax) ;*putstatic i//将val的值从寄存器写回内存
; - com.zsk.test.TestSynchronized::test@10 (line 15)
并且 monitorenter 前采用了原子操作lock cmpxchg %rsi,(%rdi)进行中间值的交换。
如果交换成功,则执行 goto 直接退出当前函数。如果失败,执行 jne 跳转指令,继续循环执行,直到成功为止。
4.5. 可见性
在 monitor enter 后临界区开始前的地方插入一个获取屏障,在临界区结束后 moniter exit 前的地方插入释放屏障。
获取屏障和释放屏障保证了临界区内的任何读写操作无法被重排序到临界区外

4.6. 有序性
跟 volatile 一样
在临界区结束后 moniter exit 前之前插入释放屏障使得该屏障之前的任何读写操作都先于这个 moniter exit(相当于写)被提交;
在 monitor enter 后临界区开始前插入获取屏障使得这个 monitor enter(相当于读)先于该屏障之后的任何读写操作被提交。

5. sychronized 方法原理分析
public class MultiThread2
{
private static int val = 0;
public static void main(String[] args) throws InterruptedException
{
Thread thread1 = new Thread(()->{
for (int i = 0; i < 100000; i++)
{
incr();
}
});
Thread thread2 = new Thread(()->{
for (int i = 0; i < 100000; i++)
{
decr();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(val);
}
private synchronized static void decr()
{
val--;
}
private synchronized static void incr()
{
val++;
}
}
字节码如下图:


在 VM 字节码层面并没有任何特别的指令来实现被 synchronized 修饰的方法,而是在 Class 文件的方法表中将该方法的 access_flags 字段中的 synchronized 标志位置 1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的 Class 在 JVM 的内部对象表示 Klass 做为锁对象。
6. 参考
- 精确解释 java 的 volatile 之可见性、原子性、有序性(通过汇编语言) - tantexian 的博客空间 - OSCHINA
- java 架构师课程 一节课学透 Synchronized 的设计原理 图灵学院_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili
- Guide to the Synchronized Keyword in Java | Baeldung
- 彻底理解 synchronized - 掘金
- 【死磕 Java 并发】- synchronized 的锁膨胀过程 - Java 技术驿站-Java 技术驿站
- JVM 内部细节之一:synchronized 关键字及实现细节(轻量级锁 Lightweight Locking) - JAVA Miner - 博客园
- Java synchronized 能防止指令重排序吗? - 知乎
- Synchronized 之三:Synchronized 与线程中断、线程 wait - duanxz - 博客园
Java源码分析系列笔记-2.Synchronized的更多相关文章
- Java源码分析系列之HttpServletRequest源码分析
从源码当中 我们可以 得知,HttpServletRequest其实 实际上 并 不是一个类,它只是一个标准,一个 接口而已,它的 父类是ServletRequest. 认证方式 public int ...
- Java源码分析系列
1) 深入Java集合学习系列:HashMap的实现原理 2) 深入Java集合学习系列:LinkedHashMap的实现原理 3) 深入Java集合学习系列:HashSet的实现原理 4) 深入Ja ...
- spring源码分析系列 (8) FactoryBean工厂类机制
更多文章点击--spring源码分析系列 1.FactoryBean设计目的以及使用 2.FactoryBean工厂类机制运行机制分析 1.FactoryBean设计目的以及使用 FactoryBea ...
- spring源码分析系列 (1) spring拓展接口BeanFactoryPostProcessor、BeanDefinitionRegistryPostProcessor
更多文章点击--spring源码分析系列 主要分析内容: 一.BeanFactoryPostProcessor.BeanDefinitionRegistryPostProcessor简述与demo示例 ...
- spring源码分析系列 (2) spring拓展接口BeanPostProcessor
Spring更多分析--spring源码分析系列 主要分析内容: 一.BeanPostProcessor简述与demo示例 二.BeanPostProcessor源码分析:注册时机和触发点 (源码基于 ...
- Spring Ioc源码分析系列--Bean实例化过程(一)
Spring Ioc源码分析系列--Bean实例化过程(一) 前言 上一篇文章Spring Ioc源码分析系列--Ioc容器注册BeanPostProcessor后置处理器以及事件消息处理已经完成了对 ...
- Spring Ioc源码分析系列--容器实例化Bean的四种方法
Spring Ioc源码分析系列--实例化Bean的几种方法 前言 前面的文章Spring Ioc源码分析系列--Bean实例化过程(二)在讲解到bean真正通过那些方式实例化出来的时候,并没有继续分 ...
- MyCat源码分析系列之——结果合并
更多MyCat源码分析,请戳MyCat源码分析系列 结果合并 在SQL下发流程和前后端验证流程中介绍过,通过用户验证的后端连接绑定的NIOHandler是MySQLConnectionHandler实 ...
- MyCat源码分析系列之——BufferPool与缓存机制
更多MyCat源码分析,请戳MyCat源码分析系列 BufferPool MyCat的缓冲区采用的是java.nio.ByteBuffer,由BufferPool类统一管理,相关的设置在SystemC ...
- [Tomcat 源码分析系列] (二) : Tomcat 启动脚本-catalina.bat
概述 Tomcat 的三个最重要的启动脚本: startup.bat catalina.bat setclasspath.bat 上一篇咱们分析了 startup.bat 脚本 这一篇咱们来分析 ca ...
随机推荐
- maven知识理解和生命周期
学习的技能/知识 运动 提升 不足 强化了maven的知识理解和生命周期 3公里日常跑,其中1公里破之前的记录达到3分40 没有赖床,嗯:写完的博客自己阅读又温习了一遍 下午没课,但都用来休息了.. ...
- 简单实现Android的本地文件读写,暨将List数据保存到Json文件中并读出
一.让我们从引入依赖开始 //将这两行代码添加到以上位置,其他的一般不用管 implementation 'com.google.code.gson:gson:2.8.5' implementatio ...
- datasnap的restful服务器
说真话,这玩意真的简单好用.但你要控制好: 1.内存泄漏和异常处理好: 2.有没有发现,通过服务器对数据库进行读写时,在资源管理器中,如果是sql server,就会看到连接1433的连接一直挂在那里 ...
- 支付宝当面付和微信付款码支付封装DLL
项目中需要为客户对接支付宝的当面付和微信付款码支付.场景就是软件中生成金额订单,然后扫顾客的微信付款码或者支付宝的付款码完成收款.为此封装了此DLL,可以用在其他项目中,其他人也可以直接拿来用. 最主 ...
- 解密prompt系列52. 闲聊大模型还有什么值得探索的领域
在DeepSeek-R1的开源狂欢之后,感觉不少朋友都陷入了技术舒适区,但其实当前的大模型技术只是跨进了应用阶段,可以探索的领域还有不少,所以这一章咱不聊论文了,偶尔不脚踏实地,单纯仰望天空,聊聊还有 ...
- Java 里的对象在虚拟机里面是怎么存储的?
Java 中的对象在虚拟机里的存储 在 Java 中,对象在虚拟机中的存储方式取决于 JVM 内存模型,主要存储在 堆(Heap) 中.对象的内存布局和管理方式会影响对象的创建.访问和销毁.下面详细解 ...
- web自动化基础
一.浏览器驱动 下载浏览器对应版本驱动 Chromedriver下载地址:https://npm.taobao.org/mirrors/chromedriver 下载谷歌对应版本对应系统的驱动,把下载 ...
- 代码随想录第十七天 | Leecode 654. 最大二叉树、617. 合并二叉树、700. 二叉搜索树中的搜索、98. 验证二叉搜索树
Leecode 654. 最大二叉树 题目描述 给定一个不重复的整数数组 nums . 最大二叉树 可以用下面的算法从 nums 递归地构建: 创建一个根节点,其值为 nums 中的最大值. 递归地在 ...
- 【工具】Typora中主题css修改|看了这篇,一劳永逸
真正的指南 1. 查看当前的css shift+f12,与一般浏览器调试一样,先打开控制台,查找你需要修改的地方叫什么名字.(也可以点击"视图"-"开发者工具" ...
- 【BUG】Linux目录下明明有可执行文件却提示找不到,“No such file or directory”,解决:为64位Ubuntu安装32位程序的运行架构
问题 我做了如下努力: ls显示:(能够成功显示) 修改文件名:(能够正常复制.修改.移动,并且被复制的仍然不能运行) 调整文件属性,弄成777: cat显示文件.(能够成功显示) root执行文件: ...



