Java synchronized 全面解析:从入门使用到底层原理(面试必备)
目录
1.2 线程不安全的场景(为什么需要 synchronized)
二、基础使用:synchronized 的三种用法(必掌握)
三、进阶:synchronized 的核心特性(理解这些,才算入门)
四、底层原理:synchronized 是如何实现的?(面试难点)
Q2:synchronized 有几种用法?分别锁定什么对象?
Q4:synchronized 是可重入锁吗?什么是可重入性?
Q5:synchronized 如何保证原子性、可见性、有序性?
Q6:JDK 1.6 对 synchronized 做了哪些优化?
Q7:synchronized 和 volatile 的区别?(高频中的高频)
Q8:synchronized 和 ReentrantLock 的区别?
一、入门:synchronized 是什么?解决什么问题?
1.1 核心定义
synchronized 是 Java 中的内置锁(也叫监视器锁 Monitor Lock),是一种非公平、可重入的独占锁。它可以修饰方法、代码块,通过“锁定特定对象”的方式,让多个线程排队访问共享资源,从而保证同一时刻只有一个线程能执行被锁定的代码,解决线程安全问题。
1.2 线程不安全的场景(为什么需要 synchronized)
我们先看一个没有使用 synchronized 的例子,直观感受线程不安全的问题:模拟多线程执行“自增操作”(共享变量 count 从 0 自增到 10000)。
「预期结果」:10000 + 10000 = 20000 「实际结果」:大概率是小于20000的随机数(比如18937、19562)
「原因」:count++ 不是原子操作,当两个线程同时读取到同一个 count 值(比如100),线程1自增为101、线程2自增也为101,最终写入内存的是101,相当于“少加了一次”。这种多线程竞争共享资源导致的数据错乱,就是“线程不安全”,而 synchronized 就能解决这个问题。
1.3 加上 synchronized 后的效果
我们给 increment 方法加上 synchronized 修饰,再执行代码:
public class SynchronizedDemo { // 共享变量 private static int count = 0; // 自增方法(未加锁) public static void increment() { count++; // 看似一行代码,实际是 读取count → 自增 → 写入count 三步操作(非原子) } public static void main(String[] args) throws InterruptedException { // 线程1:执行10000次自增 Thread thread1 = new Thread(() -> { for (int i = 0; i < 10000; i++) { increment(); } }); // 线程2:执行10000次自增 Thread thread2 = new Thread(() -> { for (int i = 0; i < 10000; i++) { increment(); } }); thread1.start(); thread2.start(); thread1.join(); // 等待线程1执行完毕 thread2.join(); // 等待线程2执行完毕 System.out.println("最终count值:" + count); } }「实际结果」:每次执行都是 20000,完全符合预期。
「原理」:synchronized 锁定了 increment 方法,让多个线程排队执行该方法,同一时刻只有一个线程能进入方法执行 count++,保证了自增操作的原子性,从而避免了数据错乱。
二、基础使用:synchronized 的三种用法(必掌握)
synchronized 有三种核心用法,分别是「修饰实例方法」、「修饰静态方法」、「修饰代码块」,每种用法锁定的“对象”不同,适用场景也不同,必须区分清楚(面试常考)。
2.1 用法一:修饰实例方法(锁定当前对象 this)
synchronized 修饰非静态方法(实例方法)时,锁定的是「调用该方法的对象实例」(this)。也就是说,同一个对象实例调用该方法时,会排队执行;不同对象实例调用该方法时,互不影响(因为锁定的是不同的对象)。
public class SynchronizedInstanceMethod { // 实例变量(属于对象实例的共享资源) private int num = 0; // synchronized 修饰实例方法,锁定 this(当前对象) public synchronized void add() { for (int i = 0; i < 1000; i++) { num++; } } public static void main(String[] args) throws InterruptedException { // 创建两个不同的对象实例 SynchronizedInstanceMethod obj1 = new SynchronizedInstanceMethod(); SynchronizedInstanceMethod obj2 = new SynchronizedInstanceMethod(); // 线程1:调用 obj1 的 add 方法 Thread t1 = new Thread(obj1::add); // 线程2:调用 obj2 的 add 方法 Thread t2 = new Thread(obj2::add); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("obj1.num:" + obj1.num); // 1000(预期) System.out.println("obj2.num:" + obj2.num); // 1000(预期) }「说明」:t1 锁定的是 obj1,t2 锁定的是 obj2,两个线程锁定的是不同对象,因此可以同时执行 add 方法,互不干扰,最终两个对象的 num 都能达到 1000。
「注意」:如果两个线程调用的是同一个对象实例的 add 方法,就会排队执行,比如将 t2 改为 `new Thread(obj1::add)`,那么最终 obj1.num 会是 2000。
2.2 用法二:修饰静态方法(锁定类对象 .class)
synchronized 修饰静态方法时,锁定的不是 this,而是「当前类的 Class 对象」(每个类在 JVM 中只有一个 Class 对象,全局唯一)。因此,无论创建多少个对象实例,调用该静态方法时,所有线程都会排队执行(因为锁定的是同一个 Class 对象)。
public class SynchronizedStaticMethod { // 静态变量(属于类的共享资源,全局唯一) private static int num = 0; // synchronized 修饰静态方法,锁定 SynchronizedStaticMethod.class public static synchronized void add() { for (int i = 0; i < 1000; i++) { num++; } } public static void main(String[] args) throws InterruptedException { SynchronizedStaticMethod obj1 = new SynchronizedStaticMethod(); SynchronizedStaticMethod obj2 = new SynchronizedStaticMethod(); // 线程1:调用 obj1 的静态 add 方法 Thread t1 = new Thread(obj1::add); // 线程2:调用 obj2 的静态 add 方法 Thread t2 = new Thread(obj2::add); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("num:" + num); // 2000(预期) }「说明」:虽然 t1 和 t2 调用的是不同对象实例的 add 方法,但 add 是静态方法,锁定的是 SynchronizedStaticMethod.class(全局唯一),因此两个线程会排队执行 add 方法,最终 num 累加为 2000。
2.3 用法三:修饰代码块(锁定指定对象,灵活度最高)
synchronized 修饰代码块时,需要手动指定「锁定的对象」(括号内填写锁定对象),语法:`synchronized(锁定对象) { 需同步的代码 }`。这种用法的灵活度最高,可以只锁定“需要同步的代码片段”(而非整个方法),减少锁的粒度,提升程序性能。
常见的锁定对象有两种:this(当前对象)、Class 对象,也可以是自定义的对象(只要保证多线程锁定的是同一个对象即可)。
案例1:锁定 this(等价于修饰实例方法,但粒度更细)
public class SynchronizedBlockThis { private int num = 0; public void add() { // 只锁定自增相关的代码块,而非整个 add 方法 synchronized (this) { for (int i = 0; i < 1000; i++) { num++; } } // 非同步代码(多个线程可同时执行) System.out.println("非同步代码执行"); } public static void main(String[] args) throws InterruptedException { SynchronizedBlockThis obj = new SynchronizedBlockThis(); Thread t1 = new Thread(obj::add); Thread t2 = new Thread(obj::add); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("num:" + obj.num); // 2000(预期) }「说明」:只有 synchronized 代码块内的自增操作会被同步,代码块外的打印语句可以被多个线程同时执行,相比修饰整个实例方法,减少了锁的范围,提升了效率。
案例2:锁定 Class 对象(等价于修饰静态方法)
public class SynchronizedBlockClass { private static int num = 0; public void add() { // 锁定 Class 对象,全局唯一 synchronized (SynchronizedBlockClass.class) { for (int i = 0; i < 1000; i++) { num++; } } } public static void main(String[] args) throws InterruptedException { SynchronizedBlockClass obj1 = new SynchronizedBlockClass(); SynchronizedBlockClass obj2 = new SynchronizedBlockClass(); Thread t1 = new Thread(obj1::add); Thread t2 = new Thread(obj2::add); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("num:" + num); // 2000(预期) }案例3:锁定自定义对象(常用于多线程共享资源的同步)
public class SynchronizedBlockCustom { private int num = 0; // 自定义锁定对象(必须保证多线程使用的是同一个对象) private final Object lock = new Object(); public void add() { // 锁定自定义对象 lock synchronized (lock) { for (int i = 0; i < 1000; i++) { num++; } } } public static void main(String[] args) throws InterruptedException { SynchronizedBlockCustom obj = new SynchronizedBlockCustom(); Thread t1 = new Thread(obj::add); Thread t2 = new Thread(obj::add); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("num:" + obj.num); // 2000(预期) }「注意」:自定义锁定对象建议用 final 修饰,避免对象被修改(如果 lock 被重新赋值,会导致多个线程锁定不同对象,失去同步效果)。
三种用法的核心区别(面试高频)
用法 | 锁定对象 | 同步范围 | 适用场景 |
|---|---|---|---|
修饰实例方法 | this(当前对象实例) | 整个实例方法 | 实例变量的同步(多个线程操作同一个对象的实例变量) |
修饰静态方法 | 当前类的 Class 对象 | 整个静态方法 | 静态变量的同步(多个线程操作同一个类的静态变量) |
修饰代码块 | 手动指定(this、Class、自定义对象) | 代码块内的代码 | 灵活控制同步粒度(只同步需要的代码,提升效率) |
三、进阶:synchronized 的核心特性(理解这些,才算入门)
synchronized 能保证线程安全,核心依赖于它的三个特性:原子性、可见性、有序性,除此之外,它还是「可重入锁」、「非公平锁」,这些特性是面试的核心考点,必须逐一搞懂。
3.1 原子性(核心特性)
「原子性」:指一个操作或多个操作,要么全部执行完成,要么全部不执行,中间不会被其他线程打断。就像“银行转账”,从A账户扣钱、给B账户加钱,这两个操作必须同时完成,不能只执行一半。
synchronized 能保证「被锁定的代码块/方法」的原子性:同一时刻只有一个线程能进入被锁定的代码,避免了多线程同时执行导致的操作中断,从而保证了原子性。
「补充」:Java 中还有一些原子类(如 AtomicInteger),也能保证原子性,但底层是 CAS 机制,和 synchronized 的实现方式不同,后续会单独写博客拆解。
3.2 可见性(核心特性)
「可见性」:指当一个线程修改了共享变量的值后,其他线程能立即看到这个修改后的值。
为什么会有“不可见性”?因为每个线程都有自己的「工作内存」,线程读取共享变量时,会先从主内存中拷贝一份到工作内存,后续操作都基于工作内存中的副本;当线程修改了共享变量后,会先更新工作内存中的副本,再异步刷新到主内存中。如果其他线程此时读取共享变量,可能读取的还是主内存中未被更新的值,导致数据错乱。
synchronized 能保证可见性:当线程释放锁时,会将工作内存中修改后的共享变量值「强制刷新到主内存」;当线程获取锁时,会「清空工作内存中的共享变量副本」,重新从主内存中读取最新的值。这样就保证了多个线程看到的共享变量值是一致的。
3.3 有序性(核心特性)
「有序性」:指程序执行的顺序按照代码的先后顺序执行。但在 JVM 中,为了提升性能,会对代码进行「指令重排序」(在不影响单线程执行结果的前提下,调整指令的执行顺序)。
指令重排序在单线程环境下没问题,但在多线程环境下,可能会导致程序执行结果异常。例如:
// 共享变量 private static int a = 0; private static boolean flag = false; // 线程1执行 public static void thread1() { a = 1; // 指令1 flag = true; // 指令2 } // 线程2执行 public static void thread2() { if (flag) { // 指令3 System.out.println(a); // 可能输出 0,而非 1 } }「原因」:JVM 可能会将线程1的指令1和指令2重排序(因为单线程下,先执行 flag=true 再执行 a=1,结果不变),如果线程1先执行 flag=true,线程2此时读取 flag 为 true,就会打印 a 的值(此时 a 还未被赋值为1,还是0),导致结果异常。
synchronized 能保证有序性:被 synchronized 锁定的代码块,会禁止指令重排序,保证代码按照编写顺序执行。同时,synchronized 还能保证“happens-before”关系(后续单独拆解),进一步保证多线程环境下的有序性。
3.4 可重入性(重要特性)
「可重入性」:指一个线程已经获取了某个锁,当它再次需要获取该锁时(比如递归调用被锁定的方法),可以直接获取,不会导致死锁。
synchronized 是可重入锁,我们用递归案例验证:
public class SynchronizedReentrant { // 锁定 this public synchronized void methodA() { System.out.println("进入 methodA"); methodB(); // 递归调用 methodB(同样锁定 this) } // 同样锁定 this public synchronized void methodB() { System.out.println("进入 methodB"); } public static void main(String[] args) { SynchronizedReentrant obj = new SynchronizedReentrant(); obj.methodA(); // 正常执行,不会死锁 } }「执行结果」:
进入 methodA 进入 methodB
「说明」:线程调用 methodA 时,获取了 this 锁;在 methodA 中调用 methodB 时,因为 methodB 锁定的也是 this 锁,而线程已经持有该锁,所以可以直接进入 methodB,不会死锁。这就是可重入性的体现。
「补充」:synchronized 的可重入性,底层是通过「锁计数器」实现的:线程第一次获取锁时,计数器置为1;再次获取该锁时,计数器加1;释放锁时,计数器减1;当计数器为0时,锁才会被真正释放,其他线程才能获取。
3.5 非公平性(重要特性)
「公平锁」:多个线程等待同一个锁时,按照“先到先得”的顺序获取锁,不会出现线程饥饿(某个线程一直得不到锁)。
「非公平锁」:多个线程等待同一个锁时,线程获取锁的顺序是随机的,不一定是先到先得,可能存在某个线程多次获取锁,而其他线程长期等待的情况。
synchronized 是「非公平锁」:当锁被释放时,等待队列中的线程不会按照排队顺序获取锁,而是随机竞争,这样做的目的是「提升程序性能」(避免了线程排队的开销)。
「补充」:Java 中的 ReentrantLock 可以实现公平锁和非公平锁(默认非公平),后续会对比 synchronized 和 ReentrantLock 的区别。
四、底层原理:synchronized 是如何实现的?(面试难点)
前面我们讲了 synchronized 的用法和特性,接下来拆解最核心的底层原理 —— 它到底是靠什么实现“锁定对象、保证线程安全”的?
synchronized 的底层实现依赖于 JVM 的「监视器锁(Monitor)」,而 Monitor 的实现又和「对象头」、「字节码指令」密切相关。我们从“字节码层面”和“JVM 层面”两个维度拆解。
4.1 字节码层面:synchronized 的指令体现
我们先看一段简单的代码,反编译它的字节码,看看 synchronized 在字节码层面是如何表现的。
public class SynchronizedBytecode { private final Object lock = new Object(); public void test() { synchronized (lock) { System.out.println("synchronized 代码块"); } } }使用 javac 编译后,再用 javap -v 反编译,查看 test 方法的字节码(核心部分):
public void test(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: aload_0 1: getfield #2 // Field lock:Ljava/lang/Object; 4: dup 5: astore_1 6: monitorenter // 进入同步代码块,获取锁 7: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 10: ldc #4 // String synchronized 代码块 12: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 15: aload_1 16: monitorexit // 退出同步代码块,释放锁 17: goto 25 20: astore_2 21: aload_1 22: monitorexit // 异常情况下释放锁 23: aload_2 24: athrow 25: return「核心指令解析」:
- monitorenter:当线程执行到 monitorenter 指令时,会尝试获取当前锁定对象的 Monitor 锁。
- 如果 Monitor 的计数器为 0(表示锁未被持有),则将计数器置为 1,当前线程成为 Monitor 的所有者;
- 如果当前线程已经是 Monitor 的所有者(可重入),则将计数器加 1;
- 如果 Monitor 被其他线程持有,则当前线程会阻塞,直到 Monitor 的计数器为 0,再尝试获取锁。
- monitorexit:当线程执行到 monitorexit 指令时,会释放 Monitor 锁,将 Monitor 的计数器减 1。当计数器减为 0 时,Monitor 会被释放,阻塞的线程可以尝试获取锁。
「注意」:字节码中会有两个 monitorexit 指令,第一个用于正常退出同步代码块时释放锁,第二个用于异常情况下释放锁(避免锁泄露)。
「补充」:如果是 synchronized 修饰方法(实例方法/静态方法),字节码层面不会出现 monitorenter 和 monitorexit 指令,而是会在方法的访问标志中增加 ACC_SYNCHRONIZED 标志,JVM 会根据该标志自动为方法添加 Monitor 的获取和释放逻辑(本质和代码块一致)。
4.2 JVM 层面:Monitor 锁与对象头
前面提到,synchronized 锁定的是“对象”,而 Monitor 锁是和“对象”绑定的 —— 每个 Java 对象在 JVM 中都有一个「对象头」,对象头中包含了「Monitor 锁的相关信息」,这也是 synchronized 能锁定对象的核心原因。
4.2.1 Java 对象头结构
Java 对象头由两部分组成(32位 JVM 为例):
- Mark Word(标记字,32位):存储对象的哈希码、分代年龄、锁状态标志、持有锁的线程 ID 等信息(核心部分);
- Klass Pointer(类型指针,32位):指向对象所属类的 Class 对象,JVM 通过该指针确定对象的类型。
其中,Mark Word 的结构会随着「锁状态」的变化而变化,synchronized 的锁升级机制也和 Mark Word 密切相关。Mark Word 的默认结构(无锁状态)如下:
位偏移 | 内容 | 说明 |
|---|---|---|
0-1位 | 锁状态标志 | 01:无锁状态;00:轻量级锁;10:重量级锁;11:偏向锁 |
2位 | 是否偏向锁 | 0:无偏向;1:偏向锁 |
3-23位 | 对象的哈希码 | 无锁/偏向锁状态下存储 |
24-30位 | 对象的分代年龄 | GC 回收时使用 |
31位 | 无意义(固定为0) | - |
4.2.2 Monitor 锁的本质
Monitor 是 JVM 内部的一个数据结构,也叫「监视器」,每个对象都有一个对应的 Monitor(可以理解为“锁的载体”)。Monitor 的结构如下(简化版):
- Owner:存储当前持有 Monitor 的线程 ID,只有一个线程能成为 Owner;
- EntryList:存储等待获取 Monitor 锁的线程队列(阻塞状态);
- WaitSet:存储调用 wait() 方法后释放锁、进入等待状态的线程队列。
「Monitor 工作流程」:
- 线程执行 monitorenter 指令时,尝试获取 Monitor 的 Owner 权限:
- 如果 Owner 为空,当前线程成为 Owner,Monitor 计数器置为 1;
- 如果当前线程已是 Owner,计数器加 1(可重入);
- 如果 Owner 是其他线程,当前线程进入 EntryList 队列,阻塞等待。
- 线程执行 monitorexit 指令时,Monitor 计数器减 1:
- 如果计数器减为 0,释放 Owner 权限(置为 null),唤醒 EntryList 中一个阻塞的线程,让其尝试获取锁;
- 如果计数器不为 0(可重入场景),则当前线程仍持有 Owner 权限。
4.3 锁升级机制(JDK 1.8 优化,重点面试考点)
在 JDK 1.6 之前,synchronized 是「重量级锁」—— 每次获取和释放锁都需要调用操作系统的互斥量(Mutex),切换线程状态(用户态 ↔ 内核态),开销很大,效率很低。
为了优化 synchronized 的性能,JDK 1.6 及以后引入了「锁升级机制」:synchronized 的锁会从「无锁」→「偏向锁」→「轻量级锁」→「重量级锁」逐步升级,根据线程竞争的激烈程度动态调整,减少锁的开销。
锁升级的核心依据:「线程竞争的激烈程度」—— 竞争越激烈,锁的级别越高,开销也越大,但能保证线程安全。
4.3.1 1. 无锁状态
「场景」:没有线程竞争共享资源,对象处于无锁状态。
「Mark Word 特征」:锁状态标志为 01,是否偏向锁为 0,存储对象的哈希码和分代年龄。
4.3.2 2. 偏向锁(减少无竞争下的锁开销)
「场景」:只有一个线程多次获取同一个锁,没有其他线程竞争。
「核心思想」:既然只有一个线程获取锁,就不需要每次都执行 monitorenter/monitorexit 指令(避免内核态切换),而是将锁“偏向”于这个线程,后续该线程再次获取锁时,直接通过 Mark Word 验证即可,无需竞争。
「升级过程」:
- 线程第一次获取锁时,将 Mark Word 中的「是否偏向锁」设为 1,「锁状态标志」设为 11,同时存储当前线程的 ID;
- 该线程后续再次获取锁时,检查 Mark Word 中的线程 ID 是否为当前线程 ID:
- 如果是,直接获取锁,无需其他操作;
- 如果不是,说明有其他线程竞争,偏向锁升级为轻量级锁。
「补充」:偏向锁是 JDK 1.6 默认开启的,可通过 JVM 参数 -XX:-UseBiasedLocking 关闭。
4.3.3 3. 轻量级锁(减少多线程竞争下的阻塞开销)
「场景」:有多个线程竞争同一个锁,但竞争不激烈(线程交替获取锁,没有长时间阻塞)。
「核心思想」:当偏向锁被打破(有其他线程竞争),锁升级为轻量级锁,此时线程获取锁时,不再使用 Monitor(避免内核态切换),而是通过「CAS 机制」竞争锁。
「升级过程」:
- 线程获取锁时,JVM 会在当前线程的栈帧中创建一个「锁记录(Lock Record)」,存储锁定对象的 Mark Word 副本;
- 线程通过 CAS 操作,将锁定对象的 Mark Word 替换为「指向当前线程锁记录的指针」:
- 如果 CAS 成功,说明当前线程获取到锁,Mark Word 锁状态标志设为 00;
- 如果 CAS 失败,说明有其他线程正在竞争锁,此时会自旋(循环尝试 CAS)几次,如果自旋成功,仍能获取锁;如果自旋失败,说明竞争激烈,轻量级锁升级为重量级锁。
「补充」:自旋锁的目的是避免线程阻塞(内核态切换开销大),适合竞争不激烈的场景;如果竞争激烈,自旋会浪费 CPU 资源,此时升级为重量级锁更合适。
4.3.4 4. 重量级锁(最终锁形态)
「场景」:多个线程激烈竞争同一个锁,自旋多次仍无法获取锁。
「核心思想」:重量级锁依赖于 JVM 中的 Monitor 和操作系统的互斥量(Mutex),线程获取不到锁时,会进入阻塞状态(放弃 CPU 资源),直到锁被释放后被唤醒。
「Mark Word 特征」:锁状态标志为 10,存储指向 Monitor 的指针,此时线程获取锁会触发内核态切换,开销最大。
锁升级总结(表格梳理)
锁状态 | 适用场景 | 实现方式 | 开销 |
|---|---|---|---|
无锁 | 无线程竞争 | 无锁机制 | 无 |
偏向锁 | 单线程重复获取锁 | Mark Word 存储线程 ID | 极低 |
轻量级锁 | 多线程交替竞争,竞争不激烈 | CAS 机制 + 自旋 | 较低 |
重量级锁 | 多线程激烈竞争 | Monitor + 操作系统互斥量 | 较高 |
六、面试高频问题
结合前面的内容,整理了 synchronized 最常考的面试题,每个问题都给出简洁、精准的答案(适合面试时直接回答)。
Q1:synchronized 的作用是什么?
答:保证多线程环境下共享资源的原子性、可见性和有序性,避免数据错乱,实现线程安全。
Q2:synchronized 有几种用法?分别锁定什么对象?
答:三种用法:
- 修饰实例方法:锁定当前对象 this;
- 修饰静态方法:锁定当前类的 Class 对象;
- 修饰代码块:锁定括号内手动指定的对象(this、Class 对象、自定义对象)。
Q3:synchronized 是公平锁还是非公平锁?
答:非公平锁。当锁被释放时,等待队列中的线程不会按照排队顺序获取锁,而是随机竞争,目的是提升程序性能。
Q4:synchronized 是可重入锁吗?什么是可重入性?
答:是可重入锁。可重入性指一个线程已经获取了某个锁,当它再次需要获取该锁时(如递归调用),可以直接获取,不会导致死锁。底层通过锁计数器实现。
Q5:synchronized 如何保证原子性、可见性、有序性?
答:
- 原子性:同一时刻只有一个线程能进入被锁定的代码,避免操作中断;
- 可见性:释放锁时强制刷新工作内存到主内存,获取锁时清空工作内存,重新读取主内存;
- 有序性:禁止指令重排序,保证代码按编写顺序执行。
Q6:JDK 1.6 对 synchronized 做了哪些优化?
答:引入了「锁升级机制」,从无锁 → 偏向锁 → 轻量级锁 → 重量级锁逐步升级,根据线程竞争激烈程度动态调整锁的级别,减少锁的开销;同时引入了自旋锁、偏向锁等优化,提升了 synchronized 的性能。
Q7:synchronized 和 volatile 的区别?(高频中的高频)
答:核心区别:volatile 只能保证可见性和有序性,不能保证原子性;synchronized 能保证原子性、可见性和有序性。
特性 | synchronized | volatile |
|---|---|---|
原子性 | 支持 | 不支持 |
可见性 | 支持 | 支持 |
有序性 | 支持 | 支持(禁止指令重排序) |
锁机制 | 独占锁(可重入、非公平) | 无锁机制 |
Q8:synchronized 和 ReentrantLock 的区别?
答:
- 锁的实现:synchronized 是 JVM 内置锁,ReentrantLock 是 Java 代码实现的锁;
- 公平性:synchronized 只能是非公平锁,ReentrantLock 可指定公平/非公平锁;
- 灵活性:ReentrantLock 支持更多功能(如中断锁、超时锁、条件变量),synchronized 更简洁;
- 性能:JDK 1.8 后两者性能差距不大,synchronized 更易维护,ReentrantLock 灵活性更高。
七、总结
synchronized 是 Java 并发编程的基础,也是面试必考的知识点,从入门到底层,核心要点可以总结为:
- 「用法」:三种用法(实例方法、静态方法、代码块),锁定对象不同,同步范围不同;
- 「特性」:原子性、可见性、有序性、可重入性、非公平性;
- 「底层」:依赖 JVM 的 Monitor 锁,通过 monitorenter/monitorexit 指令和对象头实现;
- 「优化」:JDK 1.8 锁升级机制(无锁→偏向锁→轻量级锁→重量级锁),减少开销;
- 「避坑」:锁定同一对象、避免可变对象、缩小锁粒度