一、选锁的核心原则(先明确这 3 点)
- 先无锁,再轻锁,最后重锁:能不用锁就不用(如原子类),能用轻量级锁就不用重量级锁,减少锁带来的性能开销。
- 匹配读写比例:多读少写、多写少读、读写均衡,对应不同的锁策略。
- 控制锁粒度:锁的范围越小(如只锁代码块而非整个方法)、锁的对象越具体(如分段锁),性能越好。
二、分场景的锁选择方案(附性能对比)
场景 1:无竞争 / 极低竞争(如单线程、偶尔并发)
-
推荐选择:无锁(Atomic 原子类) > 偏向锁(synchronized 自动优化)
-
核心原因:无锁通过 CAS 实现,无上下文切换开销;synchronized 会自动升级为偏向锁,几乎无性能损耗。
-
代码示例(AtomicInteger 无锁) :
java
运行
// 无锁,CAS 原子操作,适合低竞争计数场景 AtomicInteger count = new AtomicInteger(0); // 原子自增,无需加锁,性能远高于 synchronized count.incrementAndGet(); -
性能优势:无锁 > 偏向锁 > 显式锁,无上下文切换、无锁竞争开销。
场景 2:多读少写(如缓存、配置读取、数据查询)
-
推荐选择:乐观锁(CAS) > 读写锁(ReentrantReadWriteLock) > StampedLock(JDK 8+)
-
核心原因 :
- 读写锁:读锁共享(多个读线程并行),写锁排他,解决 "读多写少" 的锁竞争问题;
- StampedLock:支持 "乐观读"(无需加锁,仅验证版本),性能比读写锁更高(但使用稍复杂);
- 乐观锁:适合几乎无写操作的场景,完全无锁竞争。
-
性能对比:StampedLock(乐观读) > 读写锁 > synchronized/ReentrantLock
-
代码示例(StampedLock 乐观读) :
java
运行
import java.util.concurrent.locks.StampedLock; public class StampedLockDemo { private final StampedLock lock = new StampedLock(); private int value = 0; // 乐观读(核心优化点) public int getValue() { // 1. 获取乐观读戳记(无锁) long stamp = lock.tryOptimisticRead(); int currentValue = value; // 2. 验证读期间是否有写操作(无写则直接返回,有写则升级为读锁) if (!lock.validate(stamp)) { // 升级为悲观读锁 stamp = lock.readLock(); try { currentValue = value; } finally { lock.unlockRead(stamp); } } return currentValue; } // 写操作(排他锁) public void setValue(int newValue) { long stamp = lock.writeLock(); try { value = newValue; } finally { lock.unlockWrite(stamp); } } }
场景 3:多写少读(如订单创建、数据入库)
- 推荐选择:非公平锁(ReentrantLock 默认) > synchronized(重量级锁) > 公平锁
- 核心原因 :
- 非公平锁:无需排队,线程抢锁成功率更高,减少上下文切换,性能比公平锁高 10%-20%;
- synchronized:JDK 1.6 后优化(轻量级锁 / 偏向锁),性能接近 ReentrantLock,但灵活性低;
- 公平锁:严格按顺序排队,避免线程饥饿,但性能低,仅适合需保证顺序的场景。
- 性能对比:非公平 ReentrantLock > synchronized > 公平 ReentrantLock
- 关键优化:缩小锁粒度(如只锁写入逻辑,而非整个方法)。
场景 4:高并发且读写均衡(如秒杀、库存扣减)
-
推荐选择:分段锁(ConcurrentHashMap) > 自旋锁 > 分布式锁(Redis/ZooKeeper)
-
核心原因 :
- 分段锁:将锁拆分为多个小锁(如 ConcurrentHashMap 把数据分成 16 段,每段独立加锁),降低锁竞争;
- 自旋锁:线程抢锁失败时不阻塞,而是循环重试(适合短时间锁持有),减少上下文切换;
- 分布式锁:跨 JVM 并发时使用,优先选 Redis(性能高),其次 ZooKeeper(可靠性高)。
-
代码示例(分段锁思想) :
java
运行
import java.util.concurrent.locks.ReentrantLock; // 模拟分段锁:将库存按商品ID分段,每段独立加锁 public class SegmentLockDemo { // 分段数(一般设为 CPU 核心数*2) private static final int SEGMENT_COUNT = 16; private final ReentrantLock[] locks = new ReentrantLock[SEGMENT_COUNT]; private final int[] stock = new int[SEGMENT_COUNT]; public SegmentLockDemo() { // 初始化分段锁 for (int i = 0; i < SEGMENT_COUNT; i++) { locks[i] = new ReentrantLock(); stock[i] = 1000; // 每段初始库存 } } // 扣减库存:只锁对应分段 public void deductStock(int goodsId) { // 计算商品ID对应的分段 int segment = Math.abs(goodsId % SEGMENT_COUNT); ReentrantLock lock = locks[segment]; lock.lock(); try { if (stock[segment] > 0) { stock[segment]--; System.out.println("商品" + goodsId + "库存扣减后:" + stock[segment]); } } finally { lock.unlock(); } } }
三、性能优化的关键技巧
- 避免锁膨胀:synchronized 偏向锁→轻量级锁→重量级锁升级后不可逆,高并发下尽量减少锁竞争(如拆分锁、无锁设计)。
- 减少锁持有时间:仅在核心临界区加锁(如读取数据不加锁,仅写入加锁)。
- 使用并发容器替代手动加锁:优先用 ConcurrentHashMap、CopyOnWriteArrayList 等,底层已做锁优化。
- 避免死锁:按固定顺序加锁、设置锁超时(ReentrantLock 的 tryLock (超时时间))。
总结
- 先判断并发特征:读多写少选读写锁 / StampedLock,多写少读选非公平 ReentrantLock,高并发选分段锁;
- 优先无锁 / 轻锁:低竞争用 Atomic 原子类,避免无脑用 synchronized;
- 控制锁粒度和持有时间:这是提升锁性能最有效的手段,比选择锁类型更重要。