synchronized 锁的范围:对象锁、类锁与代码块锁

synchronized 加在不同的地方,锁住的范围完全不同,互斥规则也不一样。。本文通过内存图和代码演示,讲清楚三种锁的区别。


目录


一、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 未加锁,不参与锁规则

总结规则:

  1. 加锁方法只和同类型的加锁方法互斥(对象锁之间,类锁之间)
  2. 对象锁和类锁互不影响
  3. 不加锁的方法不受任何影响,任何时候都可以被任意线程调用

六、代码块锁------最灵活的加锁方式

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 篇系列全部完结,感谢追更!如有疑问欢迎评论区交流,点赞是最好的支持。

相关推荐
x***r1511 小时前
linux安装 jdk-8u291-linux-x64.tar.gz 详细步骤(解压配置环境变量)
java
极光代码工作室2 小时前
基于SpringBoot的校园论坛系统
java·springboot·web开发·后端开发
XS0301062 小时前
Spring Bean 作用域 & 生命周期
java·后端·spring
NagatoYukee2 小时前
Spring Security基础部分学习
java·学习·spring
彦为君2 小时前
JavaSE-07-异常机制
java·开发语言·后端·python·spring
_Aaron___3 小时前
Spring AI 接入 MCP:工具调用不是“能调就行”,关键是边界治理
java·人工智能·spring
我是一颗柠檬3 小时前
【MySQL全面教学】MySQL性能优化实战Day13(2026年)
数据库·后端·sql·mysql·性能优化·database
向量引擎3 小时前
从零起步,如何打造专属向量引擎 API 中转工作流?
java·服务器·前端