synchronized加在不同的地方,锁住的范围完全不同,互斥规则也不一样。。本文通过内存图和代码演示,讲清楚三种锁的区别。
目录
- [一、synchronized 的三种用法](#一、synchronized 的三种用法)
- 二、理解锁的本质:先画内存图
- 三、对象锁------锁住的是对象实例
- [四、类锁------锁住的是 Class 对象](#四、类锁——锁住的是 Class 对象)
- 五、规则汇总:哪些会互斥,哪些不会
- 六、代码块锁------最灵活的加锁方式
- 七、代码块锁的特殊性:锁住整个对象
- 八、面试题精讲
- 九、内存图图
- 十、小结
一、synchronized 的三种用法
java
public class Example {
// 用法一:修饰普通(非静态)方法 → 对象锁
public synchronized void method1() { }
// 用法二:修饰静态方法 → 类锁
public static synchronized void method3() { }
// 用法三:修饰代码块 → 锁指定对象
public void method5() {
synchronized (this) { // 锁当前对象
// ...
}
synchronized (Example.class) { // 锁类对象
// ...
}
synchronized (someObj) { // 锁任意引用对象
// ...
}
}
}
三种用法对应两类锁:对象锁 和 类锁 ,加上更灵活的 代码块锁。
二、理解锁的本质:先画内存图
以下面这个类为例:
java
public class MyClass {
// 非静态加锁方法(对象锁)
public synchronized void m1() { }
public synchronized void m2() { }
// 静态加锁方法(类锁)
public static synchronized void m3() { }
public static synchronized void m4() { }
// 不加锁的方法
public void m5() { }
public static void m6() { }
}
java
MyClass x1 = new MyClass();
MyClass x2 = new MyClass();
对应的内存结构:
方法区(Method Area)
├── 类常量池(MyClass 的类信息)
│ ├── m1() [加锁] ←── x1、x2 共享同一份类定义
│ ├── m2() [加锁]
│ ├── m3() [加锁,静态] ←── 归属于类对象,不属于实例
│ ├── m4() [加锁,静态]
│ ├── m5() [不加锁]
│ └── m6() [不加锁,静态]
└── 静态常量池(存储 m3、m4 的静态引用)
堆(Heap)
├── x1 对象(MyClass 实例)
│ ├── 非静态方法指针(指向方法区中的 m1、m2)
│ └── 实例字段...
└── x2 对象(MyClass 实例)
├── 非静态方法指针(指向方法区中的 m1、m2)
└── 实例字段...
核心结论:
- 非静态方法:每个对象(x1、x2)各有一份,它们指向方法区中相同的方法定义
- 静态方法:只有一份,属于类本身,不属于任何实例
三、对象锁------锁住的是对象实例
3.1 定义
非静态方法加 synchronized,锁住的是调用该方法的对象实例。
3.2 互斥规则
同一个对象的所有加锁非静态方法,同一时刻只能有一个线程执行。
java
MyClass x1 = new MyClass();
// 场景一:两个线程调用同一对象的不同加锁方法
Thread t1 = new Thread(() -> x1.m1()); // 锁住 x1
Thread t2 = new Thread(() -> x1.m2()); // 也是 x1,阻塞等待
// 结果:t1 执行 m1 期间,t2 无法执行 m2,必须等 t1 释放锁 ❌ 不能同时执行
java
// 场景二:两个线程调用不同对象的相同方法
MyClass x1 = new MyClass();
MyClass x2 = new MyClass();
Thread t1 = new Thread(() -> x1.m1()); // 锁住 x1
Thread t2 = new Thread(() -> x2.m1()); // 锁住 x2,和 x1 无关
// 结果:互不影响,可以同时执行 ✅
记忆口诀:对象锁,锁对象,不同对象互不干扰。
3.3 为什么不同对象互不影响?
因为每个对象的非静态方法都是独立的一份 ,调用 x1.m1() 锁住的是 x1,调用 x2.m1() 锁住的是 x2,两把锁完全独立,互不干扰。
这也正好验证了"非静态方法在每个对象中都有一份"这一内存结构。
四、类锁------锁住的是 Class 对象
4.1 定义
静态方法加 synchronized,锁住的是当前类的 Class 对象(即方法区中唯一的类信息)。
4.2 互斥规则
同一个类的所有加锁静态方法,同一时刻只能有一个线程执行(无论通过哪个对象调用)。
java
// 场景:两个线程调用同一类的不同静态加锁方法
Thread t1 = new Thread(() -> MyClass.m3());
Thread t2 = new Thread(() -> MyClass.m4());
// 结果:t1 执行 m3 期间,t2 无法执行 m4,必须等待 ❌ 不能同时执行
因为 m3 和 m4 都是静态的,属于同一个 Class 对象,锁住的是同一把锁。
4.3 类锁 vs 对象锁------互不影响
类锁和对象锁是完全不同的两把锁,互不干扰:
java
Thread t1 = new Thread(() -> x1.m1()); // 对象锁,锁 x1
Thread t2 = new Thread(() -> MyClass.m3()); // 类锁,锁 MyClass.class
// 结果:互不影响,可以同时执行 ✅
五、规则汇总:哪些会互斥,哪些不会
| 线程1 调用 | 线程2 调用 | 是否互斥 | 原因 |
|---|---|---|---|
x1.m1() |
x1.m2() |
✅ 互斥 | 同一对象 x1 的加锁方法 |
x1.m1() |
x2.m1() |
❌ 不互斥 | 不同对象,各自的锁 |
x1.m1() |
x2.m2() |
❌ 不互斥 | 不同对象,各自的锁 |
MyClass.m3() |
MyClass.m4() |
✅ 互斥 | 同一类的静态加锁方法 |
x1.m1() |
MyClass.m3() |
❌ 不互斥 | 对象锁与类锁不同 |
x1.m1() |
x1.m5() |
❌ 不互斥 | m5 未加锁,不参与锁规则 |
MyClass.m3() |
MyClass.m6() |
❌ 不互斥 | m6 未加锁,不参与锁规则 |
总结规则:
- 加锁方法只和同类型的加锁方法互斥(对象锁之间,类锁之间)
- 对象锁和类锁互不影响
- 不加锁的方法不受任何影响,任何时候都可以被任意线程调用
六、代码块锁------最灵活的加锁方式
synchronized 除了修饰方法,还可以锁住任意代码块,并指定锁的对象。
6.1 基本语法
java
synchronized (锁对象) {
// 受保护的代码块
}
括号中的"锁对象"决定了锁的范围,锁对象必须是引用类型(不能是基本类型)。
6.2 三种常见写法
java
// 锁当前对象(等价于 synchronized 修饰非静态方法)
synchronized (this) {
count++;
}
// 锁类对象(等价于 synchronized 修饰静态方法)
synchronized (MyClass.class) {
staticCount++;
}
// 锁任意引用对象(更细粒度的控制)
private final Object lock = new Object();
synchronized (lock) {
count++;
}
6.3 代码块锁 vs 方法锁
代码块锁的优势在于更精确的锁范围:
java
// 方法锁:整个方法都在锁内(粒度粗)
public synchronized void process() {
doA(); // 不需要锁
doB(); // 需要锁
doC(); // 不需要锁
}
// 代码块锁:只锁需要保护的部分(粒度细,性能更好)
public void process() {
doA(); // 锁外执行
synchronized (this) {
doB(); // 只有这里需要保护
}
doC(); // 锁外执行
}
七、代码块锁的特殊性:锁住整个对象
代码块锁中,当一个对象被作为锁对象锁住时,行为和方法锁有所不同:
java
Object obj = new Object();
synchronized (obj) {
// obj 被锁住期间...
}
当 obj 被锁住时,一切针对 obj 的访问都被阻止,包括:
- 调用
obj的加锁方法 ❌ - 调用
obj的不加锁方法 ❌(这里与方法锁不同!) - 读取
obj的属性 ❌
这与对象锁不同。对象锁(方法加 synchronized)只影响同一对象的加锁方法,不加锁的方法不受影响。但代码块锁锁住对象后,连不加锁的方法也无法被访问------这是真正意义上"锁住了整个对象"。
八、面试题精讲
Q1:synchronized 修饰普通方法和静态方法有什么区别?
修饰普通(非静态)方法是对象锁,锁住的是当前调用该方法的对象实例;修饰静态方法是类锁,锁住的是当前类的 Class 对象。两者是不同的锁,互不影响。
Q2:两个线程分别调用同一对象的两个不同的 synchronized 方法,会互斥吗?
会。两个方法都是同一个对象的加锁非静态方法,共享同一把对象锁,同一时刻只能有一个线程持有该锁。
Q3:两个线程分别调用同一个类的两个不同的 static synchronized 方法,会互斥吗?
会。静态方法属于类本身,两个方法共享类锁(同一个 Class 对象),同一时刻只能有一个线程持有。
Q4:一个线程调用 synchronized 方法,另一个线程调用同一对象的普通(未加锁)方法,会互斥吗?
不会。未加锁的方法不参与 synchronized 的互斥规则,任何时候都可以被调用。
Q5:synchronized 代码块的锁对象可以是基本类型吗?
不可以。synchronized 的锁对象必须是引用类型(对象),因为 synchronized 本质上是在对象的 monitor(监视器)上加锁。基本类型没有对象头,没有 monitor。
Q6:类锁和对象锁同时存在,会互相影响吗?
不会。类锁锁的是
Class对象,对象锁锁的是实例对象,是两把完全不同的锁,互不影响。
九、内存图
synchronized 的锁范围

① 方法区里的加锁方法(粉色标记)
m3、m4、m6 是静态方法,存在方法区的静态区,整个 JVM 只有一份。其中 m3、m4 带锁,两个线程不管通过 x1 还是 x2 去调用,竞争的都是 Shop.class 这同一把类锁,必然互斥。
m6 虽然也是静态的,但没有 synchronized,任何时候都可以自由调用,不参与互斥。
② 堆里的两个对象(x1=325,x2=475)
每个对象的对象头里都有一个 Monitor(监视器),这才是对象锁真正存储的地方。x1 的 Monitor 和 x2 的 Monitor 是两个完全独立的锁:
线程1 锁住 x1 → 拿到的是 x1 对象头里的 Monitor
线程2 锁住 x2 → 拿到的是 x2 对象头里的 Monitor
两把锁毫无关系,两个线程可以同时执行 ✅
③ 栈里的方法帧(当前执行状态)
栈中可以看到 x2,325、T.main、x1,475、aaa 这几个帧。aaa 是当前正在执行的 synchronized 代码块帧,它持有的是 x2 的对象锁(475)。此时 x1(325)的锁是空闲的,任何线程都可以去竞争它,两者完全不冲突。
十、小结
| 锁类型 | 加锁位置 | 锁住的对象 | 互斥范围 |
|---|---|---|---|
| 对象锁 | 非静态方法 | 当前实例 | 同一实例的所有加锁非静态方法 |
| 类锁 | 静态方法 | Class 对象 | 同一类的所有加锁静态方法 |
| 代码块锁 | synchronized(obj){} | 指定对象 | 持有同一对象锁的所有代码块;若锁住整个对象,连不加锁的方法也受影响 |
理解了锁的范围,就能在实际开发中精确控制同步粒度,既保证线程安全,又避免不必要的性能损耗。
系列完结!导航:
- 第一篇:Java 并发编程基础------线程状态与上下文切换
- 第二篇:CPU 高速缓存深度解析------多级缓存架构与缓存行原理
- 第三篇:并发可见性问题------高速缓存 Bug 与 volatile 的本质
- 第四篇:子线程为什么只能操作引用类型变量
- 第五篇:synchronized 如何真正保证线程安全------写后读思想与正确加锁姿势
- 第六篇(本篇):synchronized 锁的范围全解析------对象锁、类锁与代码块锁
6 篇系列全部完结,感谢追更!如有疑问欢迎评论区交流,点赞是最好的支持。