25 | synchronized 和 ReentrantLock 该怎么选?
摘要:
synchronized是 JVM 内置锁,简单易用;ReentrantLock是 Java 代码实现的锁,功能更强大。本文讲清两者的区别和选型原则。
一、问题现象
很多人不知道该用哪个:
// 方式一:synchronizedpublicvoidprocess(){synchronized(this){// 业务逻辑}}// 方式二:ReentrantLockprivatefinalReentrantLocklock=newReentrantLock();publicvoidprocess(){lock.lock();try{// 业务逻辑}finally{lock.unlock();}}到底该用哪个?
二、踩坑现场
场景 1:需要超时获取锁,synchronized 做不到
// ❌ synchronized 无法设置超时,会一直等publicvoidprocess(){synchronized(this){// 如果锁被占住,会一直阻塞}}场景 2:需要可中断的锁等待
// ❌ synchronized 的等待无法被中断publicvoidprocess(){synchronized(this){// 线程在等锁的过程中,无法被 interrupt() 唤醒}}场景 3:需要公平锁
// ❌ synchronized 不保证公平性(抢锁)// 某些场景下需要公平锁(先到先得)三、原理解析
3.1 synchronized 的特点
优势:
- JVM 内置,自动加锁/释放锁(不需要手动
unlock()) - JDK 1.6 之后做了大量优化(偏向锁、轻量级锁、自旋锁),性能已接近
ReentrantLock - 代码简洁
劣势:
- 无法设置超时
- 无法中断等待
- 不支持公平锁
- 只支持独占锁(不支持共享锁)
- 锁的范围只能是代码块或方法
3.2 ReentrantLock 的特点
优势:
- 支持超时获取锁(
tryLock(timeout, unit)) - 支持可中断(
lockInterruptibly()) - 支持公平锁(构造时传
true) - 支持多条件变量(
newCondition()) - 可以尝试获取锁(
tryLock(),不阻塞)
劣势:
- 必须手动
unlock()(忘了就会死锁) - 代码比
synchronized长
3.3 功能对比表
| 功能 | synchronized | ReentrantLock |
|---|---|---|
| 自动释放锁 | ✅ | ❌(需手动) |
| 超时获取锁 | ❌ | ✅tryLock(timeout) |
| 可中断 | ❌ | ✅lockInterruptibly() |
| 公平锁 | ❌ | ✅new ReentrantLock(true) |
| 多条件变量 | ❌(只有一个隐式条件) | ✅newCondition() |
| 尝试获取锁(不阻塞) | ❌ | ✅tryLock() |
| 性能 | JDK 1.6+ 已优化,接近 | 略好(高并发下) |
四、正确写法
4.1 简单场景:用 synchronized
// ✅ 推荐:简单场景用 synchronizedpublicclassCounter{privateintcount=0;publicsynchronizedvoidincrement(){// 锁的是 thiscount++;}publicintgetCount(){returncount;// 读也需要加锁(保证可见性)}}4.2 需要超时:用 ReentrantLock
// ✅ 推荐:需要超时用 ReentrantLockpublicclassResource{privatefinalReentrantLocklock=newReentrantLock();publicvoidprocess(){try{if(lock.tryLock(5,TimeUnit.SECONDS)){// ✅ 最多等 5 秒try{// 业务逻辑}finally{lock.unlock();// ✅ 必须手动释放}}else{log.warn("获取锁超时");}}catch(InterruptedExceptione){Thread.currentThread().interrupt();// ✅ 恢复中断标志}}}4.3 需要可中断:用 lockInterruptibly()
// ✅ 推荐:需要可中断用 lockInterruptibly()publicvoidprocess(){try{lock.lockInterruptibly();// ✅ 可以被 interrupt() 唤醒try{// 业务逻辑}finally{lock.unlock();}}catch(InterruptedExceptione){log.info("任务被中断");Thread.currentThread().interrupt();}}4.4 生产者-消费者:用 Condition
// ✅ 推荐:需要多条件变量用 ConditionpublicclassBlockingQueue<T>{privatefinalList<T>queue=newArrayList<>();privatefinalintmaxSize;privatefinalReentrantLocklock=newReentrantLock();privatefinalConditionnotEmpty=lock.newCondition();privatefinalConditionnotFull=lock.newCondition();publicvoidput(Titem)throwsInterruptedException{lock.lock();try{while(queue.size()==maxSize){notFull.await();// ✅ 等待"不满"条件}queue.add(item);notEmpty.signal();// ✅ 唤醒"不空"的等待线程}finally{lock.unlock();}}publicTtake()throwsInterruptedException{lock.lock();try{while(queue.isEmpty()){notEmpty.await();// ✅ 等待"不空"条件}Titem=queue.remove(0);notFull.signal();// ✅ 唤醒"不满"的等待线程returnitem;}finally{lock.unlock();}}}五、最佳实践
✅ synchronized vs ReentrantLock 选型指南
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 简单的互斥 | synchronized | 代码简洁,自动释放锁 |
| 需要超时 | ReentrantLock.tryLock(timeout) | synchronized不支持 |
| 需要可中断 | ReentrantLock.lockInterruptibly() | synchronized不支持 |
| 生产者-消费者 | ReentrantLock+Condition | 多条件变量 |
| 读多写少 | ReentrantReadWriteLock | 读写分离,读读不互斥 |
🔍 JDK 1.6 之后的 synchronized 优化
JDK 1.6 对synchronized做了大量优化,引入了:
- 偏向锁:第一个获取锁的线程,后续无需 CAS
- 轻量级锁:短时间竞争用 CAS,不阻塞线程
- 自旋锁:获取锁失败时短暂自旋,避免挂起线程
- 重量级锁:长时间竞争才升级为操作系统互斥量
结论:JDK 1.6+,synchronized性能已接近ReentrantLock,优先用synchronized(代码简洁)。
🛠️ 读写分离场景:用 ReadWriteLock
// ✅ 读多写少场景:用 ReadWriteLockprivatefinalReadWriteLockrwLock=newReentrantReadWriteLock();privatefinalLockreadLock=rwLock.readLock();privatefinalLockwriteLock=rwLock.writeLock();publicStringgetConfig(Stringkey){readLock.lock();// ✅ 多读并发try{returnconfig.get(key);}finally{readLock.unlock();}}publicvoidsetConfig(Stringkey,Stringvalue){writeLock.lock();// ✅ 写独享try{config.put(key,value);}finally{writeLock.unlock();}}六、小结
- 简单互斥用
synchronized(JDK 1.6+ 性能已够好,代码简洁) - 需要超时/可中断/公平锁/多条件变量,用
ReentrantLock - 读多写少用
ReentrantReadWriteLock(读读并发) ReentrantLock必须手动unlock(),放在finally里- 不确定用哪个?优先
synchronized,需要高级功能再换ReentrantLock
第三辑完。—— 下一篇进入第四辑:JVM 与性能篇,首篇:toString里打印对象?小心无限递归栈溢出。