如何选择合适的锁机制来提高 Java 程序的性能?

一、选锁的核心原则(先明确这 3 点)

  1. 先无锁,再轻锁,最后重锁:能不用锁就不用(如原子类),能用轻量级锁就不用重量级锁,减少锁带来的性能开销。
  2. 匹配读写比例:多读少写、多写少读、读写均衡,对应不同的锁策略。
  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();
            }
        }
    }

三、性能优化的关键技巧

  1. 避免锁膨胀:synchronized 偏向锁→轻量级锁→重量级锁升级后不可逆,高并发下尽量减少锁竞争(如拆分锁、无锁设计)。
  2. 减少锁持有时间:仅在核心临界区加锁(如读取数据不加锁,仅写入加锁)。
  3. 使用并发容器替代手动加锁:优先用 ConcurrentHashMap、CopyOnWriteArrayList 等,底层已做锁优化。
  4. 避免死锁:按固定顺序加锁、设置锁超时(ReentrantLock 的 tryLock (超时时间))。

总结

  1. 先判断并发特征:读多写少选读写锁 / StampedLock,多写少读选非公平 ReentrantLock,高并发选分段锁;
  2. 优先无锁 / 轻锁:低竞争用 Atomic 原子类,避免无脑用 synchronized;
  3. 控制锁粒度和持有时间:这是提升锁性能最有效的手段,比选择锁类型更重要。
相关推荐
IT_陈寒2 小时前
JavaScript 性能优化:7 个 V8 引擎偏爱的编码模式让你提速 40%
前端·人工智能·后端
long3162 小时前
类与对象 | 低级别设计 (LLD)
java·spring boot·学习·程序人生·spring·设计模式·学习方法
专注于大数据技术栈2 小时前
java学习--String、StringBuilder、StringBuffer 的核心区别
java·学习
黎述寒2 小时前
Python字典和集合
python
我命由我123452 小时前
Java 开发问题:包名 ‘com.my.compressimagetest‘ 与同名的类发生冲突
java·开发语言·学习·java-ee·intellij-idea·学习方法·intellij idea
⑩-2 小时前
Sleep与Wait的区别
java·开发语言
程序员阿鹏2 小时前
List和Set的区别
java·开发语言·数据结构·后端·list
小oo呆2 小时前
【自然语言处理与大模型】LangChainV1.0入门指南:核心组件Messages
前端·javascript·easyui
CHANG_THE_WORLD2 小时前
6.2.在汇编层面,数据本身没有类型
java·服务器·汇编