如何选择合适的锁机制来提高 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. 控制锁粒度和持有时间:这是提升锁性能最有效的手段,比选择锁类型更重要。
相关推荐
r_oo_ki_e_20 小时前
java22--常用类
java·开发语言
全栈前端老曹20 小时前
【包管理】npm init 项目名后底层发生了什么的完整逻辑
前端·javascript·npm·node.js·json·包管理·底层原理
HHHHHY20 小时前
mathjs简单实现一个数学计算公式及校验组件
前端·javascript·vue.js
boooooooom20 小时前
Vue3 provide/inject 跨层级通信:最佳实践与避坑指南
前端·vue.js
一颗烂土豆20 小时前
Vue 3 + Three.js 打造轻量级 3D 图表库 —— chart3
前端·vue.js·数据可视化
linweidong20 小时前
C++ 中避免悬挂引用的企业策略有哪些?
java·jvm·c++
青莲84320 小时前
Android 动画机制完整详解
android·前端·面试
用户937611475816120 小时前
并发编程三大特性
java·后端
阿在在20 小时前
Spring 系列(二):加载 BeanDefinition 的几种方式
java·后端·spring
iReachers20 小时前
HTML打包APK(安卓APP)中下载功能常见问题和详细介绍
前端·javascript·html·html打包apk·网页打包app·下载功能