在深入探讨 Ja va 并发编程时,synchronized 是一个绕不开的核心关键字。很多人觉得它既笨重又过时,但事实上,在大多数实际场景下,它依然是解决并发问题最可靠手段之一。今天,我们就将synchronized彻底掰开揉碎,从它是什么、何时使用、如何运用,到背后的字节码与汇编原理,一次性讲清楚。
1. 什么是 synchronized
synchronized 是 Ja va 中悲观锁的一种典型实现。相较于 volatile,它属于重量级锁,但功能也更强大:能同时保证原子性、有序性和可见性。
- 重量级
会引起上下文切换(简单来说,就是会导致线程阻塞)。 - 原子性
被synchronized修饰的方法或代码块,在 JVM 视角中是一个不可分割的整体,不会被其他线程中断。 - 有序性
线程 A 释放锁之前的所有写操作,对之后成功获取锁的线程 B 都是可见的。更严谨的说法是:线程 A 对于锁 X 的释放,happens-before 线程 B 对于锁 X 的申请。这意味着 A 在释放锁之前修改的所有内容,B 获取锁后都能清晰看到。 - 可见性
当线程退出同步块时,所有在临界区内修改的共享变量都会被强制刷新回主内存。这样其他线程下次读取时就能拿到最新值。
2. 何时使用 synchronized
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 进行 10 万次递增,thread2 对 val 进行 10 万次递减,按理结果应为 0,但实际运行结果每次都不相同。
2.1.1. 原因分析
假设两个线程的执行顺序被打乱,例如:
- 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,完全不符合预期。问题根源在于“读取→计算→写回”这个三步操作并非原子性,两个线程交错执行就会导致数据混乱。
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); } }
这样一来,每次只允许一个线程进入临界区,结果就稳定为 0 了。
3. 如何使用 synchronized
synchronized 有三种用法,区别在于所锁定的对象不同。
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); } }
结果为 false,因为两个线程使用的锁对象不同,彼此不干扰,自然线程不安全。
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); } } }
使用 SychronizedTest3.class 作为锁,所有线程共用同一把锁,自然就保证了安全性。
4. synchronized 代码块原理分析
4.1. 字节码实验
在 IDEA 中运行以下代码,然后通过 Show Bytecode 插件查看字节码:
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 时,同一时刻只能由一个线程持有。所谓“持有”,其实就是做一个标记——这个标记就打在 Ja va 对象头中。
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. 对比实验
- 未加 synchronized
public class TestSynchronized { private static int i = 0; public static void main(String[] args) { test(); } private static void test() { i++; } }
- 加了 synchronized
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 之后、临界区开始之前,会插入一个 获取屏障;在临界区结束之后、monitor exit 之前,会插入一个 释放屏障。这两个屏障保证了临界区内的任何读写操作都不会被重排序到临界区之外。示意图如下:

4.6. 有序性
这一部分与 volatile 的机制类似。在临界区结束后、monitor exit 之前插入 释放屏障,确保屏障之前的任何读写操作都先于这个 monitor exit(相当于写操作)被提交;在 monitor enter 之后、临界区开始前插入 获取屏障,确保这个 monitor enter(相当于读操作)先于屏障之后的任何读写操作被提交。图示如下:

5. synchronized 方法原理分析
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++; } }
再来查看字节码,这次使用的是方法级别的 synchronized。字节码中没有 MONITORENTER/MONITOREXIT 指令,而是多了一个 ACC_SYNCHRONIZED 标志。当方法被调用时,JVM 检查到这个标志,就会自动获取和释放监视器。具体字节码如下图所示:


