“说说synchronized和ReentrantLock的区别?”
面试官问出这个问题时,我心里暗笑——太老套了!背了无数遍的八股文终于派上用场。
我清了清嗓子:“synchronized是Java关键字,ReentrantLock是类;synchronized自动释放锁,ReentrantLock需要手动释放;synchronized不可中断,ReentrantLock可以中断;synchronized非公平锁,ReentrantLock可以选公平或非公平...”
“嗯,背得不错。”面试官点点头,“那你知道为什么synchronized性能差吗?”
“因为...重量级锁?要切换内核态?”
“那你知道JDK6之后synchronized优化了吗?知道锁升级的过程吗?知道偏向锁、轻量级锁、重量级锁的区别吗?”
我:“......”
第一章:那年我写过的bug
1.1 我的“完美”设计
有一次,我们要做一个类似秒杀的活动。我的设计“很完美”:
@Service
public class SeckillService {
// 商品库存缓存
private final Map<Long, Integer> stockMap = new ConcurrentHashMap<>();
// 用户购买记录(防止重复购买)
private final Set<String> userPurchaseSet = Collections.synchronizedSet(new HashSet<>());
@Transactional
public boolean seckill(Long productId, Long userId) {
String key = productId + "_" + userId;
// 检查是否已购买
if (userPurchaseSet.contains(key)) {
return false;
}
// 检查库存
Integer stock = stockMap.get(productId);
if (stock == null || stock <= 0) {
return false;
}
// 扣减库存
stockMap.put(productId, stock - 1);
// 记录购买
userPurchaseSet.add(key);
// 创建订单(数据库操作)
orderService.createOrder(productId, userId);
return true;
}
}
“看,我用了ConcurrentHashMap和synchronizedSet,线程安全!” 我当觉得完美了。
1.2 测试结果给我当头一棒
压测一开始。监控显示:
- CPU 100% :所有CPU核心都跑满了
- 线程数爆炸 :从200个涨到2000个
- 数据库连接池耗尽 :120个连接全部被占用
1.3 我犯的三个致命错误
错误一:竞态条件
// 这两行代码不是原子的!
Integer stock = stockMap.get(productId); // 线程A和线程B同时读到stock=1
if (stock == null || stock <= 0) {
return false;
}
stockMap.put(productId, stock - 1); // 都执行put,库存变成-1!
错误二:synchronizedSet的误用
private final Set<String> userPurchaseSet = Collections.synchronizedSet(new HashSet<>());
// synchronizedSet只保证单个方法调用线程安全
// 但contains和add是两个方法调用!
if (userPurchaseSet.contains(key)) { // 线程A检查,不在
// 线程B也检查,也不在
userPurchaseSet.add(key); // 都执行add,重复购买!
}
错误三:长事务
@Transactional // 这个注解让整个方法在事务中执行
public boolean seckill(Long productId, Long userId) {
// 前面所有检查...
orderService.createOrder(productId, userId); // 数据库操作
// 事务要等这里结束才提交!
// 数据库连接被占用几十秒!
}
第二章:synchronized的真相
2.1 我以为的synchronized
我曾经以为synchronized就是“给方法加个锁,保证线程安全”:
public synchronized void doSomething() {
// 线程安全了!
}
简单、粗暴、有效。
直到我看了字节码。
2.2 synchronized的字节码真相
public synchronized void test() {
System.out.println("hello");
}
// 编译后的字节码
public synchronized void test();
Code:
0: getstatic #2 // 获取System.out
3: ldc #3 // 加载字符串"hello"
5: invokevirtual #4 // 调用println方法
8: return
等等,哪里体现了“锁”?
因为synchronized是JVM层面的实现,不是字节码指令!
方法级的synchronized,是通过方法的 ACC_SYNCHRONIZED 标志实现的:
访问标志:ACC_PUBLIC, ACC_SYNCHRONIZED
2.3 对象锁 vs 类锁
这是我曾经混淆的概念:
public class LockExample {
// 对象锁:锁的是this实例
public synchronized void instanceLock() {
// 同一个实例,多个线程串行执行
// 不同实例,可以并发执行
}
// 类锁:锁的是LockExample.class
public static synchronized void staticLock() {
// 所有实例,所有线程,串行执行
}
// 代码块锁
public void blockLock() {
// 锁对象
synchronized (this) {
// 同instanceLock
}
// 锁类
synchronized (LockExample.class) {
// 同staticLock
}
// 锁任意对象
private final Object lock = new Object();
synchronized (lock) {
// 自定义锁
}
}
}
2.4 锁升级:从偏向锁到重量级锁
这是面试必考点,也是我曾经完全不懂的点。
JDK6之后,synchronized不再是简单的“重量级锁”,而是有了锁升级机制:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
1. 偏向锁(Biased Locking)
假设场景:只有一个线程访问同步块。
// 大多数情况下,这个对象只会被一个线程访问
public class Counter {
private int count = 0;
public synchronized void increment() {
count++; // 90%的时间,只有一个线程调用这个方法
}
}
JVM会“偏向”第一个访问的线程:
- 在对象头Mark Word中记录线程ID
- 以后这个线程再来,直接进入,不需要CAS操作
- 开销极小
2. 轻量级锁(Lightweight Locking)
当有第二个线程来竞争时,升级为轻量级锁:
// 两个线程偶尔竞争
public void transfer(Counter a, Counter b) {
synchronized (a) {
synchronized (b) {
// 两个线程可能同时执行,但竞争不激烈
}
}
}
- 通过CAS操作竞争锁
- 竞争失败就自旋(忙等待)
- 适合锁持有时间短、竞争不激烈的场景
3. 重量级锁(Heavyweight Locking)
当竞争激烈时,升级为重量级锁:
// 100个线程疯狂竞争
public class HotCounter {
private int count = 0;
public synchronized void increment() {
count++; // 100个线程同时竞争这个锁
}
}
2.5 为什么synchronized性能不差?
这是我当年的误解。现在我知道了:
在低竞争场景下,synchronized性能很好:
- 偏向锁:几乎无开销
- 轻量级锁:CAS操作,比ReentrantLock的AQS简单
在高竞争场景下,所有锁性能都差:
- synchronized重量级锁
- ReentrantLock的AQS队列
- 性能差距不大
synchronized的优势:
- JVM会自动优化(锁升级、锁消除、锁粗化)
- 不会忘记释放锁
- 代码简洁
第三章:ReentrantLock的深度解析
3.1 我为什么放弃了synchronized?
在秒杀活动测出问题后,我研究了ReentrantLock。
public class SeckillServiceV2 {
private final Map<Long, Integer> stockMap = new ConcurrentHashMap<>();
private final Map<Long, ReentrantLock> productLocks = new ConcurrentHashMap<>();
public boolean seckill(Long productId, Long userId) {
// 为每个商品分配一个独立的锁
ReentrantLock lock = productLocks.computeIfAbsent(productId,
k -> new ReentrantLock());
if (!lock.tryLock()) { // 尝试获取锁,失败立即返回
return false; // 没抢到锁,秒杀失败
}
try {
// 临界区代码
Integer stock = stockMap.get(productId);
if (stock == null || stock <= 0) {
return false;
}
stockMap.put(productId, stock - 1);
return true;
} finally {
lock.unlock(); // 必须手动释放!
}
}
}
3.2 ReentrantLock的高级特性
1. 可中断锁
public void doWithInterruptibleLock() throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
try {
lock.lockInterruptibly(); // 可被中断的获取锁
// 执行业务逻辑
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
// 另一个线程可以调用 thread.interrupt() 来中断等待
2. 尝试获取锁
public boolean tryDoSomething() {
ReentrantLock lock = new ReentrantLock();
if (lock.tryLock()) { // 尝试获取锁,立即返回
try {
// 获取成功,执行业务
return true;
} finally {
lock.unlock();
}
} else {
// 获取失败,执行其他逻辑
return false;
}
}
// 带超时的尝试
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
// 100ms内获取到锁
}
3. 公平锁 vs 非公平锁
// 非公平锁(默认):性能好,但可能饥饿
ReentrantLock unfairLock = new ReentrantLock(); // 默认非公平
// 公平锁:先到先得,性能稍差
ReentrantLock fairLock = new ReentrantLock(true);
公平锁的实现:AQS(AbstractQueuedSynchronizer)维护一个FIFO队列。
3.3 Condition:更灵活的等待/通知
synchronized的wait/notify太简单:
// synchronized方式
synchronized (obj) {
while (!condition) {
obj.wait(); // 释放锁,等待
}
// 条件满足,执行业务
}
// 另一个线程
synchronized (obj) {
condition = true;
obj.notifyAll(); // 通知所有等待线程
}
ReentrantLock + Condition更灵活:
public class BoundedBuffer {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 不满条件
private final Condition notEmpty = lock.newCondition(); // 不空条件
private final Object[] items = new Object[100];
private int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await(); // 等待"不满"
}
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal(); // 通知"不空"
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // 等待"不空"
}
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal(); // 通知"不满"
return x;
} finally {
lock.unlock();
}
}
}
一个锁可以创建多个Condition,实现精准通知。
第四章:volatile的误解与真相
4.1 我曾经以为volatile是轻量级锁
“volatile保证可见性,性能比synchronized好!”——这是我当年常说的话。
然后我写了这样的代码:
public class Counter {
private volatile int count = 0;
public void increment() {
count++; // ❌ 这不是原子操作!
}
public int get() {
return count; // ✅ 可见性保证了
}
}
结果:多线程下,count的值不对。
4.2 volatile的真正作用
volatile只保证:
- 可见性:一个线程修改,其他线程立即可见
- 禁止指令重排序:防止JVM优化打乱执行顺序
// 正确使用volatile的场景1:状态标志
public class StoppableTask implements Runnable {
private volatile boolean stopped = false;
public void stop() {
stopped = true; // 其他线程立即可见
}
@Override
public void run() {
while (!stopped) { // 及时看到状态变化
// 执行业务
}
}
}
// 正确使用volatile的场景2:单例模式(双重检查锁定)
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // volatile防止重排序
}
}
}
return instance;
}
}
4.3 内存屏障:volatile的底层实现
当写volatile变量时:
StoreStore屏障
写操作
StoreLoad屏障
当读volatile变量时:
LoadLoad屏障
读操作
LoadStore屏障
这些内存屏障保证了:
- 写之前的操作不会被重排序到写之后
- 读之后的操作不会被重排序到读之前
- 写操作立即刷新到主内存
- 读操作从主内存读取最新值
第五章:ThreadLocal的内存泄漏陷阱
5.1 我的内存泄漏经历
我们的用户登录系统用了ThreadLocal:
public class UserContext {
private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
public static void setUser(User user) {
currentUser.set(user);
}
public static User getUser() {
return currentUser.get();
}
}
看起来没问题,对吧?
三个月后,生产环境频繁Full GC。
5.2 ThreadLocal的内存泄漏原理
ThreadLocal的引用关系:
Thread → ThreadLocalMap → Entry → value
↑
ThreadLocal ────┘
问题在于:Entry是弱引用指向ThreadLocal,但强引用指向value!
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value; // 强引用!
Entry(ThreadLocal<?> k, Object v) {
super(k); // 弱引用指向ThreadLocal
value = v; // 强引用指向value
}
}
当ThreadLocal被回收后:
- Entry的key变成null
- 但value还在(强引用)
- 如果线程不终止,value永远无法回收
5.3 正确使用ThreadLocal
public class SafeUserContext {
private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
public static void setUser(User user) {
currentUser.set(user);
}
public static User getUser() {
return currentUser.get();
}
// 必须清理!
public static void clear() {
currentUser.remove(); // 移除当前线程的值
}
}
// 使用try-finally确保清理
public void processRequest(Request request) {
try {
UserContext.setUser(request.getUser());
doProcess(request);
} finally {
UserContext.clear(); // 确保清理
}
}
第六章:线程池的七个死亡陷阱
6.1 我配置的“高性能”线程池
@Configuration
public class ThreadPoolConfig {
@Bean("highPerformancePool")
public ThreadPoolTaskExecutor executor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(100); // 核心100线程
executor.setMaxPoolSize(1000); // 最大1000线程
executor.setQueueCapacity(10000); // 队列10000
executor.setThreadNamePrefix("high-perf-");
executor.initialize();
return executor;
}
}
结果:数据库连接池先撑不住了。
6.2 线程池的正确配置
// CPU密集型任务
@Bean("cpuIntensivePool")
public Executor cpuIntensiveExecutor() {
// 线程数 = CPU核心数 + 1
int corePoolSize = Runtime.getRuntime().availableProcessors() + 1;
return new ThreadPoolExecutor(
corePoolSize,
corePoolSize, // 最大=核心,不扩容
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
new NamedThreadFactory("cpu-intensive"),
new ThreadPoolExecutor.AbortPolicy() // 快速失败
);
}
// IO密集型任务
@Bean("ioIntensivePool")
public Executor ioIntensiveExecutor() {
// 线程数 = CPU核心数 * (1 + 等待时间/计算时间)
// 简化:CPU核心数 * 2 ~ 5
int corePoolSize = Runtime.getRuntime().availableProcessors() * 3;
return new ThreadPoolExecutor(
corePoolSize,
corePoolSize * 2, // 可扩容
30L, TimeUnit.SECONDS,
new SynchronousQueue<>(), // 不缓冲,直接创建线程
new NamedThreadFactory("io-intensive"),
new ThreadPoolExecutor.CallerRunsPolicy() // 调用者执行
);
}
最后:回到那个面试问题
现在,我可以回答那个面试官了:
“synchronized和ReentrantLock的区别?”
“从表面看,synchronized是关键字,ReentrantLock是类;synchronized自动释放,ReentrantLock手动释放...”
“但真正的区别在于设计哲学:synchronized是JVM的‘自动驾驶’——简单、安全、有优化;ReentrantLock是‘手动挡’——灵活、强大、但容易出错。”
“选择哪个?看场景:简单同步用synchronized,复杂需求用ReentrantLock。但更重要的是——理解原理,避免踩坑。”
对于准备Java并发相关的面试,可以参考云栈社区的Java板块积累经验。因为并发编程,知道的越多,不知道的越多。