找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2262

积分

0

好友

324

主题
发表于 昨天 09:05 | 查看: 5| 回复: 0

“说说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 测试结果给我当头一棒

压测一开始。监控显示:

  1. CPU 100% :所有CPU核心都跑满了
  2. 线程数爆炸 :从200个涨到2000个
  3. 数据库连接池耗尽 :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的优势:

  1. JVM会自动优化(锁升级、锁消除、锁粗化)
  2. 不会忘记释放锁
  3. 代码简洁

第三章: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只保证:

  1. 可见性:一个线程修改,其他线程立即可见
  2. 禁止指令重排序:防止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屏障

这些内存屏障保证了:

  1. 写之前的操作不会被重排序到写之后
  2. 读之后的操作不会被重排序到读之前
  3. 写操作立即刷新到主内存
  4. 读操作从主内存读取最新值

第五章: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板块积累经验。因为并发编程,知道的越多,不知道的越多。




上一篇:生成式视频压缩(GVC)技术突破:0.02%极限压缩率重塑视频传输
下一篇:基于Kubernetes的云原生流式计算PaaS平台:降低开发与运维成本实践
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-16 00:34 , Processed in 1.244234 second(s), 44 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表