Java JUC源码分析:Synchronized详解
synchronized是Java悲观锁的实现,能保证原子性、有序性和可见性,在多线程访问共享资源时解决并发问题。有三种用法:修饰静态方法锁类对象、修饰普通方法锁实例对象、修饰代码块锁指定对象。其原理基于字节码中的monitorenter和monitorexit指令,通过对象监视器实现线程互斥。
在Java并发编程中,synchronized关键字是一个核心概念。尽管部分开发者认为它较为重量级,但在多数实际场景下,它依然是解决并发问题最可靠的手段之一。本文将从定义、适用场景、使用方法入手,并深入分析其背后的字节码与汇编原理,全面解析synchronized的工作机制。
1. 是什么
synchronized是Java中悲观锁的一种典型实现。相比volatile,它属于重量级锁,但能力更强:能同时保证原子性、有序性和可见性。
- 重量级
会引起上下文切换,即导致线程阻塞。 - 原子性
被synchronized修饰的方法或代码块,在JVM层面被视为一个整体,不会因线程切换而被中断。 - 有序性
线程A释放锁之前的所有写操作,对后续成功获取锁的线程B均可见。更严谨的解释:线程A对锁X的释放,happens-before线程B对锁X的申请。这意味着A在释放锁前修改的数据,B在获取锁后都能正确读取。 - 可见性
线程退出同步块时,所有在临界区内修改的共享变量都会被强制刷新回主内存,确保其他线程后续读取时能获取最新值。
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进行10万次自增,thread2进行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有三种用法,区别在于锁定的对象不同。
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时,同一时刻只有一个线程可以持有它。所谓“持有”,即在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. 对比实验
- 无 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检测到该标志,自动获取和释放监视器。具体字节码如下图所示:


