
🍃 予枫 :个人主页
📚 个人专栏 : 《Java 从入门到起飞》《读研码农的干货日常》《Java 面试刷题指南》
💻 Debug 这个世界,Return 更好的自己!
引言
面试Java并发,synchronized与Lock接口的对比绝对是高频考点!很多程序员只会用,却分不清两者的底层差异、适用场景,被面试官追问"什么时候用synchronized?什么时候用Lock?"时直接翻车。今天就从底层原理、核心区别、适用场景三个维度,结合案例和面试追问,帮你彻底分清两者,面试不慌、实战不踩坑!
文章目录
- 引言
- 一、前言:为什么要对比synchronized与Lock?
- [二、synchronized 与 Lock 核心区别(底层+用法,面试必答)](#二、synchronized 与 Lock 核心区别(底层+用法,面试必答))
-
- [2.1 底层实现差异(面试官重点追问)](#2.1 底层实现差异(面试官重点追问))
-
- [synchronized 底层实现](#synchronized 底层实现)
- [Lock 底层实现](#Lock 底层实现)
- [2.2 用法差异(结合代码案例,一看就会)](#2.2 用法差异(结合代码案例,一看就会))
-
- [synchronized 用法(3种场景,简单易用)](#synchronized 用法(3种场景,简单易用))
- [Lock 用法(需手动释放,灵活度高)](#Lock 用法(需手动释放,灵活度高))
- [2.3 核心特性差异(实战重点)](#2.3 核心特性差异(实战重点))
- 三、适用场景对比(实战选型指南,避免踩坑)
-
- [3.1 优先用 synchronized 的场景](#3.1 优先用 synchronized 的场景)
- [3.2 优先用 Lock 的场景](#3.2 优先用 Lock 的场景)
- [3.3 选型总结(图文可视化)](#3.3 选型总结(图文可视化))
- 四、面试官追问环节(实战必备,拉开差距)
-
- [追问1:synchronized 和 Lock 的性能对比?JDK1.6后synchronized为什么性能提升这么多?](#追问1:synchronized 和 Lock 的性能对比?JDK1.6后synchronized为什么性能提升这么多?)
- 追问2:Lock的tryLock()和lock()方法有什么区别?tryLock()的优势是什么?
- 追问3:ReentrantLock为什么叫可重入锁?和synchronized的可重入性有什么区别?
- 追问4:公平锁和非公平锁的区别?synchronized和Lock默认是什么锁?
- 追问5:使用Lock时,为什么必须在finally块中释放锁?
- 五、总结
一、前言:为什么要对比synchronized与Lock?
在Java并发编程中,synchronized和Lock都是实现线程同步、保证线程安全的核心方式,但两者的设计理念、底层实现、使用场景截然不同。
- synchronized是Java内置关键字,简单易用、自动释放锁,适合大多数基础同步场景;
- Lock是Java.util.concurrent.locks包下的接口,需手动释放锁、功能更灵活,适合复杂并发场景。
💡 小提示:面试时,面试官不仅会问"两者有什么区别",还会追问"为什么这么设计""实际项目中怎么选",建议点赞收藏,吃透这篇,轻松应对所有相关追问!
二、synchronized 与 Lock 核心区别(底层+用法,面试必答)
两者的区别主要集中在底层实现、锁特性、用法三个维度,用表格对比更清晰,面试时直接按这个框架回答,逻辑更清晰!
| 对比维度 | synchronized | Lock接口(以ReentrantLock为例) |
|---|---|---|
| 底层实现 | 依赖JVM的Monitor监视器锁,基于对象头 | 依赖AQS(AbstractQueuedSynchronizer)框架,基于CAS操作 |
| 锁释放 | 自动释放(代码执行完/抛出异常时自动释放) | 手动释放(必须在finally块中调用unlock(),否则会造成死锁) |
| 锁类型 | 非公平锁(默认),不可手动指定 | 可指定公平锁/非公平锁(构造方法传入boolean值) |
| 可中断性 | 不可中断,线程获取锁时会一直阻塞,除非获取到锁或被中断 | 可中断(调用lockInterruptibly()方法,可响应中断) |
| 尝试获取锁 | 不可尝试,一旦调用,必须等待锁释放 | 可尝试获取锁(tryLock()方法,超时可放弃,避免死锁) |
| 条件变量 | 无专门条件变量,需通过wait()/notify()/notifyAll()配合 | 有Condition接口,可实现多条件唤醒(精准唤醒特定线程) |
| 性能 | JDK1.6优化后(偏向锁、轻量级锁),性能接近Lock;高并发下略逊于Lock | 高并发场景下性能更优,灵活度更高 |
2.1 底层实现差异(面试官重点追问)
synchronized 底层实现
synchronized的底层依赖JVM的Monitor(监视器锁),关联对象头的Mark Word,之前在synchronized核心原理中详细讲过,这里重点对比Lock:
- 当线程获取synchronized锁时,本质是获取对象对应的Monitor所有权;
- 释放锁时,自动释放Monitor,无需手动操作,JVM会处理异常场景下的锁释放。
Lock 底层实现
Lock接口的核心实现类是ReentrantLock,底层依赖AQS框架(抽象队列同步器):
- AQS内部维护一个volatile修饰的状态变量(state),用于表示锁的持有状态;
- 线程通过CAS操作修改state的值,获取锁(state从0变为1)和释放锁(state从1变为0);
- AQS维护一个等待队列,未获取到锁的线程会进入队列等待,避免忙等,提升性能。
2.2 用法差异(结合代码案例,一看就会)
synchronized 用法(3种场景,简单易用)
java
// 场景1:修饰普通方法(锁this)
public synchronized void syncMethod() {
// 同步代码
System.out.println("synchronized修饰普通方法");
}
// 场景2:修饰静态方法(锁Class对象)
public static synchronized void syncStaticMethod() {
// 同步代码
System.out.println("synchronized修饰静态方法");
}
// 场景3:修饰代码块(显式指定锁对象)
public void syncBlock() {
Object lock = new Object();
synchronized (lock) {
// 同步代码
System.out.println("synchronized修饰代码块");
}
}
✅ 优势:无需手动释放锁,代码简洁,不易出错,适合简单同步场景。
Lock 用法(需手动释放,灵活度高)
以ReentrantLock(最常用的Lock实现类)为例:
java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo {
// 1. 创建Lock对象,可指定公平锁(true)/非公平锁(false,默认)
private final Lock lock = new ReentrantLock(true);
public void lockMethod() {
// 2. 手动获取锁
lock.lock();
try {
// 3. 同步代码
System.out.println("Lock接口实现同步");
} finally {
// 4. 手动释放锁(必须在finally中,避免异常导致死锁)
lock.unlock();
}
}
// 尝试获取锁(超时放弃,避免死锁)
public void tryLockMethod() throws InterruptedException {
// 尝试获取锁,超时时间3秒,获取失败则放弃
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
System.out.println("尝试获取锁成功");
} finally {
lock.unlock();
}
} else {
System.out.println("尝试获取锁失败,超时放弃");
}
}
}
⚠️ 注意:Lock必须手动释放锁,若忘记在finally中调用unlock(),会导致锁无法释放,造成死锁,这是新手最容易踩的坑!
2.3 核心特性差异(实战重点)
- 锁的公平性:synchronized只能是非公平锁,Lock可自由选择(公平锁适合对顺序要求高的场景,非公平锁性能更优);
- 可中断性:Lock的lockInterruptibly()方法可中断线程的锁等待,避免线程一直阻塞(比如需要停止某个线程时,synchronized无法做到);
- 条件唤醒:Lock的Condition接口可实现精准唤醒,比如生产者-消费者模型中,可分别唤醒生产者和消费者,而synchronized的notify()只能随机唤醒一个线程,notifyAll()唤醒所有线程,效率较低。
三、适用场景对比(实战选型指南,避免踩坑)
很多人分不清两者的使用场景,其实核心原则是:简单场景用synchronized,复杂场景用Lock,具体拆解如下:
3.1 优先用 synchronized 的场景
- 简单同步场景:比如单线程修改共享变量、简单的方法同步,代码简洁,无需复杂的锁操作;
- 不需要灵活控制锁的场景:无需中断锁等待、无需尝试获取锁、无需多条件唤醒,synchronized自动释放锁,不易出错;
- 低并发场景:低并发下,synchronized的性能和Lock差距不大,且使用更简单,开发效率更高。
示例:简单的计数器同步(适合用synchronized)
java
public class Counter {
private int count = 0;
// 简单同步,用synchronized更简洁
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
3.2 优先用 Lock 的场景
- 高并发场景:高并发下,Lock的性能更优,AQS框架的等待队列机制比synchronized的Monitor更高效;
- 需要灵活控制锁的场景:
- 需指定公平锁/非公平锁(比如秒杀场景,需要公平分配锁,避免线程饥饿);
- 需中断锁等待(比如用户取消操作时,中断线程的锁等待,避免资源浪费);
- 需尝试获取锁(比如超时获取锁,避免线程一直阻塞,造成死锁);
- 多条件唤醒场景:比如生产者-消费者模型,需要分别唤醒生产者和消费者,用Lock的Condition接口更高效。
示例:生产者-消费者模型(适合用Lock)
java
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ProducerConsumer {
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity = 5;
private final Lock lock = new ReentrantLock();
// 两个条件变量:队列满(生产者等待)、队列空(消费者等待)
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
// 生产者
public void produce(int value) throws InterruptedException {
lock.lock();
try {
// 队列满,生产者等待
while (queue.size() == capacity) {
notFull.await();
}
queue.offer(value);
System.out.println("生产者生产:" + value);
// 唤醒消费者(队列非空)
notEmpty.signal();
} finally {
lock.unlock();
}
}
// 消费者
public int consume() throws InterruptedException {
lock.lock();
try {
// 队列空,消费者等待
while (queue.isEmpty()) {
notEmpty.await();
}
int value = queue.poll();
System.out.println("消费者消费:" + value);
// 唤醒生产者(队列非满)
notFull.signal();
return value;
} finally {
lock.unlock();
}
}
}
3.3 选型总结(图文可视化)
简单场景(无灵活锁需求、低并发)
复杂场景(高并发、灵活锁需求)
优点:简洁、自动释放锁、不易出错
优点:高性能、可控制、多条件唤醒
并发同步需求
场景复杂度
优先用synchronized
优先用Lock
适合基础同步场景
适合复杂并发场景
💡 互动提示:评论区说说你项目中用synchronized还是Lock?遇到过哪些选型坑?收藏这张流程图,实战选型时直接对照,不踩坑!
四、面试官追问环节(实战必备,拉开差距)
这部分是核心亮点,比纯八股文更有实战价值,提前准备好,面试时直接应对!
追问1:synchronized 和 Lock 的性能对比?JDK1.6后synchronized为什么性能提升这么多?
- 性能对比:JDK1.6之前,synchronized是重量级锁,性能远低于Lock;JDK1.6对synchronized进行优化(引入偏向锁、轻量级锁、自旋锁等),性能接近Lock;高并发场景下,Lock的性能略优于synchronized(因为AQS的等待队列机制更高效)。
- synchronized性能提升原因:
- 引入偏向锁、轻量级锁,减少重量级锁的使用(避免内核态切换);
- 引入自旋锁、自适应自旋锁,减少线程阻塞;
- 实现锁消除、锁粗化,减少不必要的锁操作;
- 优化Monitor的实现,提升锁的获取和释放效率。
追问2:Lock的tryLock()和lock()方法有什么区别?tryLock()的优势是什么?
- lock():获取锁时,若锁已被持有,线程会一直阻塞,直到获取到锁,不可中断;
- tryLock():尝试获取锁,若获取成功返回true,失败返回false,不会阻塞;可指定超时时间(tryLock(long timeout, TimeUnit unit)),超时后放弃获取锁。
- 优势:避免线程一直阻塞,可有效防止死锁(比如超时后放弃获取锁,释放资源),适合高并发场景下的锁竞争。
追问3:ReentrantLock为什么叫可重入锁?和synchronized的可重入性有什么区别?
两者都是可重入锁(同一线程可多次获取同一把锁),区别在于实现方式:
- synchronized的可重入性:底层通过Monitor的计数器实现,线程第一次获取锁时计数器为1,再次获取计数器加1,释放时计数器减1,直到为0释放锁;
- ReentrantLock的可重入性:底层通过AQS的state变量实现,state初始为0,线程获取锁时state加1,再次获取时state继续加1,释放时state减1,直到为0释放锁。
- 相同点:都支持可重入,避免同一线程多次获取锁导致死锁。
追问4:公平锁和非公平锁的区别?synchronized和Lock默认是什么锁?
- 区别:
- 公平锁:严格按照线程等待顺序获取锁,不会出现线程饥饿,但性能较低(需要维护等待队列的顺序);
- 非公平锁:线程释放锁后,新到来的线程可优先获取锁,无需排队,性能更高,但可能导致某些线程长期无法获取锁(线程饥饿)。
- 默认锁类型:
- synchronized:默认是非公平锁,不可手动指定;
- Lock(ReentrantLock):默认是非公平锁,可通过构造方法传入true指定为公平锁。
追问5:使用Lock时,为什么必须在finally块中释放锁?
因为Lock需要手动释放锁,若同步代码块中抛出异常,线程会直接退出,若未在finally中释放锁,锁会一直被持有,导致其他线程无法获取锁,造成死锁。
- 示例:若未在finally中释放锁,抛出异常后锁无法释放
java
// 错误示例(易造成死锁)
public void wrongLockMethod() {
lock.lock();
// 若此处抛出异常,unlock()不会执行,锁无法释放
System.out.println("同步代码");
lock.unlock();
}
// 正确示例(必须在finally中释放)
public void rightLockMethod() {
lock.lock();
try {
System.out.println("同步代码");
} finally {
lock.unlock(); // 无论是否抛出异常,都会释放锁
}
}
五、总结
synchronized和Lock都是Java并发编程的核心同步方式,核心区别和选型要点总结如下:
- 核心区别:底层实现(Monitor vs AQS)、锁释放(自动 vs 手动)、锁特性(灵活度不同)、性能(高并发下Lock更优);
- 选型原则:简单场景用synchronized(简洁、不易出错),复杂场景用Lock(灵活、高性能);
- 面试重点:底层实现差异、性能对比、适用场景、可重入性、锁的公平性。
掌握这些内容,无论是面试中的对比题,还是实战中的选型,都能从容应对。建议结合代码练习,加深理解,避免死记硬背,真正做到学以致用。
📌 最后提示:如果觉得这篇文章对你有帮助,点赞+收藏,关注我(予枫),后续持续更新Java并发面试干货,带你避开面试坑、吃透底层原理!评论区留下你的面试经历或项目选型经验,一起交流学习~