深度剖析synchronized:从用法到底层,吃透Java并发锁的核心
深度剖析synchronized:从用法到底层,吃透Java并发锁的核心
在Java并发编程中,synchronized是最基础、最常用的同步工具,也是面试中必考的核心知识点。无论是初级开发者口中的“加锁能保证线程安全”,还是中高级面试中被追问的“锁升级原理”“底层实现机制”,synchronized始终是绕不开的重点。很多开发者只停留在“会用”的表层,对其底层逻辑、锁优化细节一知半解,不仅面试容易翻车,在生产环境中也可能因误用导致死锁、性能瓶颈等问题。
本文将从“是什么→怎么用→底层实现→锁升级→常见误区→面试高频”六个维度,深度剖析synchronized的核心逻辑,用通俗的语言拆解复杂原理,搭配代码示例和实战细节,帮你彻底吃透这把Java内置的“并发安全锁”。
一、核心认知:synchronized到底是什么?
synchronized是Java语言内置的互斥同步锁,也被称为“内置锁”或“监视器锁(Monitor)”,其核心作用是保证多线程环境下共享资源的原子性、可见性和执行结果的有序性,解决多线程并发访问导致的数据错乱、超卖、脏读等问题。
用一个生活场景就能快速理解:办公室里的打印机(共享资源),多个人(多线程)需要使用,同一时间只能有一个人操作,否则会出现文件错乱;synchronized就相当于打印机的“锁”,想要使用打印机,必须先拿到锁,使用完再归还,其他人才能竞争锁继续使用——这就是synchronized的核心逻辑:通过互斥性,保证同一时间只有一个线程执行同步代码。
与Lock锁相比,synchronized的优势在于上手简单、无需手动释放锁(异常或代码执行完毕会自动释放),安全性更高,是Java并发编程的“入门必备工具”;其劣势在JDK 1.6之前较为明显(性能开销大),但经过JVM的多次优化(偏向锁、轻量级锁等),现在性能已接近Lock锁,在大多数场景下均可优先使用。
二、核心用法:3种使用方式,锁对象决定作用范围
synchronized的用法看似简单,但不同用法的锁对象、作用范围完全不同,这是面试基础必考点,也是开发中避免误用的关键。核心有3种使用方式,结合代码示例逐一拆解,明确每种用法的锁对象和适用场景。
1. 修饰实例方法:锁对象为当前实例(this)
当synchronized修饰实例方法时,锁对象是当前调用该方法的对象实例(this),而非类本身。其作用范围是整个实例方法,只有拿到当前对象实例锁的线程,才能执行该方法。
public class SynchronizedDemo {
// 实例方法加锁,锁对象 = this(当前实例)
public synchronized void syncInstanceMethod() {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 执行实例同步方法");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SynchronizedDemo demo1 = new SynchronizedDemo();
SynchronizedDemo demo2 = new SynchronizedDemo();
// 线程1调用demo1的实例方法
new Thread(() -> demo1.syncInstanceMethod(), "线程1").start();
// 线程2调用demo2的实例方法(锁对象不同,无竞争,并行执行)
new Thread(() -> demo2.syncInstanceMethod(), "线程2").start();
}
}
关键细节(面试必答):
-
同一对象实例的所有synchronized修饰的实例方法,共享同一把锁——线程1调用对象A的method1,线程2调用对象A的method2,会竞争同一把锁,串行执行。
-
不同对象实例的实例方法,锁对象不同,互不干扰——线程1调用对象A的method1,线程2调用对象B的method1,无锁竞争,可并行执行。
2. 修饰静态方法:锁对象为当前类的Class对象
当synchronized修饰静态方法时,锁对象是当前类的Class对象(每个类在JVM中只有一个Class对象,全局唯一)。其作用范围是整个静态方法,无论创建多少个类的实例,调用静态同步方法都会竞争同一把锁。
public class SynchronizedDemo {
// 静态方法加锁,锁对象 = SynchronizedDemo.class(类对象)
public static synchronized void syncStaticMethod() {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 执行静态同步方法");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SynchronizedDemo demo1 = new SynchronizedDemo();
SynchronizedDemo demo2 = new SynchronizedDemo();
// 线程1调用demo1的静态方法
new Thread(() -> demo1.syncStaticMethod(), "线程1").start();
// 线程2调用demo2的静态方法(锁对象为类对象,全局唯一,串行执行)
new Thread(() -> demo2.syncStaticMethod(), "线程2").start();
}
}
关键细节(面试必答):
-
静态同步方法与实例同步方法,锁对象不同,互不干扰——线程1调用静态方法,线程2调用实例方法,无锁竞争,可并行执行。
-
类对象是全局唯一的,因此静态同步方法的锁是“全局锁”,适合保护类级别的共享资源(如静态变量)。
3. 修饰代码块:锁对象为显式指定的对象
这是实际开发中最灵活、最推荐的用法,可精准控制锁粒度,避免锁范围过大导致的性能问题,也是面试高频考点。synchronized修饰代码块时,锁对象由开发者显式指定,可是任意Java对象(推荐用private final修饰的独立对象,避免锁对象被修改导致锁失效)。
public class SynchronizedDemo {
// 推荐:自定义锁对象,private final保证唯一性和不可修改
private final Object lock = new Object();
private int count = 0;
public void increment() {
// 非同步代码(无锁,可并行执行,提升并发效率)
System.out.println(Thread.currentThread().getName() + " 准备修改计数");
// 仅锁定核心同步逻辑,缩小锁粒度
synchronized (lock) {
count++; // 共享资源修改,必须加锁保证原子性
System.out.println(Thread.currentThread().getName() + " 计数:" + count);
}
}
public static void main(String[] args) {
SynchronizedDemo demo = new SynchronizedDemo();
// 多个线程竞争同一把lock锁,串行执行同步代码块
for (int i = 0; i < 3; i++) {
new Thread(demo::increment, "线程" + i).start();
}
}
}
关键细节(面试必答):
-
仅同步代码块内的逻辑受锁保护,非同步代码可并行执行,锁粒度最细,性能最优。
-
常见锁对象选择:this(当前实例)、类对象(SynchronizedDemo.class)、自定义锁对象(推荐,避免与其他同步逻辑冲突)。
用法总结(面试速记)
记住3句话,轻松区分3种用法:
-
修饰实例方法:锁是this,同一实例串行,不同实例并行;
-
修饰静态方法:锁是Class对象,全局唯一,所有实例共享;
-
修饰代码块:锁是显式指定对象,锁粒度最细,灵活度最高。
三、底层实现:synchronized的“锁”到底是什么?
想要彻底理解synchronized,必须搞懂其底层实现——synchronized的锁机制基于JVM的管程(Monitor)模型,而锁的具体信息则存储在锁对象的对象头中。下面从“对象内存布局”“字节码实现”“Monitor原理”三个层面,拆解底层逻辑。
1. 前置基础:Java对象的内存布局
在HotSpot虚拟机中,一个Java对象在堆内存中的存储结构分为三个部分,其中对象头是synchronized实现锁的核心载体:
-
实例数据:存储对象的成员变量(包括父类继承的成员变量),按照数据类型长度对齐排列;
-
对齐填充:HotSpot虚拟机要求对象的起始地址必须是8字节的整数倍,不足时通过对齐填充补全,仅起到占位作用;
-
对象头:存储对象的核心元信息,分为两部分——Klass Pointer(类型指针,指向类元数据)和Mark Word(标记字段,存储锁状态、线程ID等信息)。
其中,Mark Word是整个synchronized锁实现的核心,它是一个动态的数据结构,会根据锁状态的不同复用64个bit位(64位JVM),以此节省内存开销。不同锁状态下,Mark Word的存储内容如下(重点记锁标志位):
| 锁状态 | 偏向锁位 | 锁标志位 | 64bit位存储内容(从高位到低位) |
|---|---|---|---|
| 无锁状态 | 0 | 01 | 未使用(25bit) + 哈希码(31bit) + 分代年龄(4bit) |
| 偏向锁状态 | 1 | 01 | 线程ID(54bit) + epoch(2bit) + 分代年龄(4bit) |
| 轻量级锁状态 | 无 | 00 | 指向线程栈中锁记录的指针(62bit) |
| 重量级锁状态 | 无 | 10 | 指向重量级锁ObjectMonitor对象的指针(62bit) |
关键说明:无锁与偏向锁的锁标志位均为01,通过偏向锁位区分;锁升级的过程,本质上就是Mark Word中存储内容与锁标志位的切换过程。
2. 字节码层面的实现
synchronized在字节码层面的实现分为两种形式,对应不同的使用方式:
(1)修饰代码块:monitorenter + monitorexit
当synchronized修饰代码块时,编译器会在同步代码块的开始位置插入monitorenter指令,在结束位置(正常执行和异常执行路径)插入monitorexit指令。
核心逻辑:线程执行到monitorenter时,尝试获取锁(即获取Monitor的所有权);执行到monitorexit时,释放锁。插入两个monitorexit指令的目的,是确保无论代码是否抛出异常,锁都能被正确释放,避免死锁。
(2)修饰方法:ACC_SYNCHRONIZED标志
当synchronized修饰实例方法或静态方法时,字节码层面不会插入monitorenter和monitorexit指令,而是在方法的访问标志中添加ACC\_SYNCHRONIZED标志。
核心逻辑:线程调用该方法时,会先检查方法是否携带ACC_SYNCHRONIZED标志;若携带,则自动获取对应锁对象的Monitor,方法执行完成(或抛出异常)后,自动释放Monitor。
3. 核心底层:Monitor(管程)原理
Monitor是synchronized底层的核心机制,译为“管程”或“监视器”,是一种用于实现线程互斥与协作的机制。每个Java对象都内置一个Monitor(可理解为“锁的容器”),当线程尝试获取锁时,本质上是在获取该对象对应的Monitor的所有权。
Monitor的核心结构(简化版):
-
Owner:当前持有Monitor的线程,同一时刻只有一个线程能成为Owner;
-
EntryList:等待获取Monitor的线程队列,线程进入该队列后会处于阻塞状态;
-
WaitSet:调用wait()方法后释放Monitor的线程队列,线程处于等待状态,需被notify()/notifyAll()唤醒后重新进入EntryList竞争锁。
Monitor的工作流程:
-
线程1执行同步代码,尝试获取Monitor,此时Monitor的Owner为null,线程1成为Owner,执行同步代码;
-
线程2尝试获取Monitor,此时Owner为线程1,线程2进入EntryList,阻塞等待;
-
线程1执行完同步代码,释放Monitor(Owner变为null),EntryList中的线程2被唤醒,竞争Monitor所有权;
-
若线程1在同步代码中调用wait()方法,会释放Monitor,进入WaitSet,等待被其他线程唤醒。
四、性能优化:JDK 1.6后的锁升级机制
在JDK 1.6之前,synchronized被称为“重量级锁”,因为其底层依赖操作系统的互斥量(mutex),线程切换需要从用户态切换到内核态,开销巨大。为了解决这个问题,JDK 1.6引入了锁升级机制,JVM会根据锁的竞争激烈程度,从低到高逐步升级锁的级别,以此优化性能。
锁升级的完整路径为:无锁 → 偏向锁 → 轻量级锁 → 重量级锁,且升级过程是单向不可逆的(只能从低级别向高级别升级,无法降级);锁完全释放后,对象会回到无锁状态,下一次加锁会重新触发升级流程。
1. 无锁状态(初始状态)
对象刚被创建时,处于无锁状态,Mark Word存储对象的哈希码、分代年龄等信息,锁标志位为01,偏向锁位为0。此时没有线程持有锁,任何线程都可以尝试获取锁。
2. 偏向锁:消除无竞争场景的同步开销
偏向锁的核心设计目标是消除无竞争场景下的所有同步开销,连CAS操作都尽量省略。其适用场景是:单线程反复进入同步块,完全无竞争。
核心逻辑:当第一个线程访问同步块时,JVM会通过CAS操作将该线程的ID记录到Mark Word中,同时将偏向锁位设为1、锁标志位保持01,进入偏向锁状态。之后该线程再次进入同步块时,无需任何同步操作(无需CAS、无需阻塞),直接进入,彻底消除无竞争场景下的同步开销。
注意:JDK 15及之后版本已默认禁用偏向锁并标记为废弃,如需使用,需手动添加JVM启动参数:\-XX:\+UseBiasedLocking \-XX:BiasedLockingStartupDelay=0;此外,一旦计算对象的哈希码,会禁用偏向锁(因为Mark Word的空间被哈希码占用,无法存储线程ID)。
偏向锁的撤销:当其他线程尝试获取已被偏向的锁时,JVM会暂停持有偏向锁的线程,检查该线程是否仍活跃:若已退出同步块,锁会恢复到无锁状态;若仍活跃,偏向锁会被撤销,升级为轻量级锁。
3. 轻量级锁:应对低竞争、交替访问场景
轻量级锁的适用场景是:多个线程交替访问同步块(无真正的并发竞争),核心是通过CAS操作避免线程阻塞,减少内核态切换开销。
升级过程:
-
线程进入同步块时,若锁处于无锁或偏向锁状态,会在当前线程的栈帧中创建一个“锁记录(Lock Record)”,并将Mark Word的内容复制到锁记录中(称为Displaced Mark Word);
-
线程通过CAS操作,将对象头的Mark Word替换为指向锁记录的指针,锁标志位设为00,进入轻量级锁状态;
-
若CAS操作成功,线程获取锁,执行同步代码;若失败(说明有其他线程竞争锁),则线程会进行自旋(循环尝试获取锁),避免立即阻塞。
关键优化:自旋优化。失败的线程不会立即阻塞,而是自旋尝试获取锁,自旋次数由JVM自适应调整(基于上次获取锁的情况)。自旋的优势是避免线程切换的开销,劣势是自旋过程会消耗CPU资源,因此仅适用于低竞争场景。
4. 重量级锁:应对高竞争场景
当锁的竞争激烈(多个线程同时竞争锁,自旋失败)时,轻量级锁会升级为重量级锁,此时锁的实现依赖操作系统的互斥量(mutex),线程会进入阻塞状态,开销较大。
升级触发条件:
-
自旋次数超过JVM默认阈值(默认10次);
-
等待获取锁的线程数超过CPU核数的一半。
重量级锁的特点:线程阻塞会触发操作系统的线程调度,上下文切换开销大,但能保证高竞争场景下的线程安全;一旦升级为重量级锁,即使后续竞争减少,也不会降级为轻量级锁或偏向锁。
锁升级总结(面试必背)
用一句话概括:无锁看竞争,单线程偏向,交替访问轻量,高竞争重量级。锁升级的核心设计思想,是“按需升级”,尽可能避免重量级锁带来的高开销,在不同竞争场景下实现最优性能。
五、核心特性:synchronized的3大并发保障
面试中常问:“synchronized能保证原子性、可见性、有序性吗?”很多人会回答“都能保证”,这是错误的。正确结论是:synchronized能保证原子性、可见性,不能保证代码层面的指令重排(有序性),但能保证执行结果的有序性。
1. 保证原子性
原子性是指:一个操作或多个操作,要么全部执行且执行过程不被中断,要么全部不执行。synchronized通过“互斥性”天然保证原子性——同一时间只有一个线程能执行同步代码,任何线程都无法打断正在执行的同步操作,因此同步代码块内的操作是不可分割的。
例如:count++(包含“读取-修改-写入”三步),在多线程环境下若不加锁,会出现数据错乱;而用synchronized包裹后,同一时间只有一个线程能执行count++,保证了操作的原子性。
2. 保证可见性
可见性是指:一个线程对共享变量的修改,能立即被其他线程看到。synchronized通过“锁的释放-获取”机制保证可见性:
-
线程释放锁时,JVM会自动将该线程工作内存中的共享变量修改,刷新到主内存中;
-
线程获取锁时,JVM会自动将该线程工作内存中的共享变量清空,重新从主内存中读取最新值。
这就保证了,任何线程获取锁后,看到的都是共享变量的最新值,避免了因工作内存与主内存数据不一致导致的可见性问题。
3. 不保证指令重排,但保证执行结果有序
有序性是指:程序执行的顺序与代码编写的顺序一致,避免指令重排导致的逻辑混乱。synchronized不直接禁止指令重排——JVM仍可以在同步代码块内部进行指令重排,只要重排后的结果与顺序执行一致(即as-if-serial语义)。
但synchronized能保证“执行结果的有序性”:由于锁的互斥性,同步代码块会被串行执行,其他线程无法看到同步代码块内的中间执行状态,因此即使发生指令重排,最终的执行结果也与顺序执行一致,不会出现并发乱序问题。
六、常见误区与实战避坑
很多开发者在使用synchronized时,会因理解不透彻导致误用,出现死锁、锁失效、性能瓶颈等问题。以下是4个最常见的误区,结合实战场景拆解,帮你避开坑点。
误区1:synchronized能保证所有有序性
错误认知:认为synchronized能禁止所有指令重排,保证代码顺序执行。
正确认知:synchronized不禁止同步代码块内部的指令重排,仅保证执行结果的有序性;若需要禁止指令重排(如单例模式DCL),需结合volatile关键字。
误区2:锁对象可以随意选择,不会影响锁效果
错误用法:用String、Integer等不可变对象作为锁对象,或用非final修饰的对象作为锁对象。
避坑建议:优先使用private final修饰的自定义对象作为锁对象(如private final Object lock = new Object());避免用String常量(常量池复用导致锁冲突)、Integer(自动装箱导致锁对象变化)、this(可能与其他实例方法锁冲突)作为锁对象。
误区3:锁粒度越大,安全性越高
错误认知:将整个方法加锁,认为这样更安全,无需考虑锁粒度。
避坑建议:锁粒度越小,并发性能越好。尽量只对“共享资源修改”的核心逻辑加锁(即修饰代码块),避免对整个方法加锁,减少线程阻塞时间,提升并发效率。
误区4:synchronized会导致死锁,尽量不用
错误认知:因担心死锁,拒绝使用synchronized,转而使用更复杂的Lock锁。
避坑建议:死锁的产生不是因为synchronized本身,而是因为“多个线程持有不同锁,且互相等待对方释放锁”。只要遵循“锁的顺序一致”(多个线程获取多把锁时,按固定顺序获取)、“避免锁嵌套”(不在同步代码块内获取其他锁),就能避免死锁;synchronized上手简单、自动释放锁,在大多数场景下仍是最优选择。
七、面试高频考点:必背问答
结合前文内容,整理synchronized面试最常考的5个问题,直接背诵即可应对面试。
1. synchronized的三种使用方式及锁对象?
答:① 修饰实例方法,锁对象是this(当前实例);② 修饰静态方法,锁对象是当前类的Class对象;③ 修饰代码块,锁对象是显式指定的对象。
2. synchronized能保证原子性、可见性、有序性吗?
答:能保证原子性和可见性;不能保证代码层面的指令重排(有序性),但能保证执行结果的有序性。
3. JDK 1.6对synchronized做了哪些优化?锁升级流程是什么?
答:优化包括:引入偏向锁、轻量级锁、自旋优化、批量重偏向/批量撤销。锁升级流程:无锁 → 偏向锁 → 轻量级锁 → 重量级锁,单向不可逆。
4. synchronized和volatile的区别?
答:① 作用范围:synchronized修饰方法/代码块,volatile修饰变量;② 原子性:synchronized保证原子性,volatile不保证;③ 可见性:两者都保证,但实现方式不同(synchronized靠锁的释放-获取,volatile靠内存屏障);④ 有序性:synchronized保证结果有序,volatile禁止特定指令重排;⑤ 性能:volatile无锁开销小,synchronized低竞争下性能接近volatile,高竞争下开销大。
5. synchronized的底层实现原理?
答:基于JVM的Monitor(管程)模型,锁信息存储在对象头的Mark Word中;字节码层面,代码块通过monitorenter/monitorexit指令实现,方法通过ACC_SYNCHRONIZED标志实现;JDK 1.6后通过锁升级机制优化性能,减少重量级锁的开销。
八、总结
synchronized作为Java内置的并发锁,其核心是“互斥性”,通过Monitor机制和锁升级优化,兼顾了线程安全和性能。从用法上看,三种使用方式的核心区别在于锁对象的不同;从底层上看,锁的本质是对象头Mark Word的状态切换;从特性上看,它能保证原子性、可见性和执行结果的有序性,是解决多线程并发问题的基础工具。
理解synchronized,不仅能应对面试中的高频问题,更能在生产环境中合理使用它,避免死锁、性能瓶颈等问题。掌握“用法→底层→优化→误区”的完整逻辑,才算真正吃透这把Java并发编程的“入门锁”。
(注:文档部分内容可能由 AI 生成)
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)