java中atomicinteger通过cas实现乐观锁,核心是“先干活、再核对、冲突重试”,需用循环包裹compareandset或使用updateandget等封装方法,避免aba问题可选atomicstampedreference,高并发计数推荐longadder,但需权衡一致性与性能。

Java里用AtomicInteger实现乐观锁最直接
乐观锁在Java中不靠数据库或synchronized,核心是“先干活、再核对、冲突就重试”。AtomicInteger这类原子类就是标准实现——它底层调用Unsafe.compareAndSwapInt(CAS),每次更新都检查当前值是否仍等于预期旧值,是才写入,否则失败。
常见错误是把getAndIncrement()当普通变量++用,却没处理返回值或忽略失败重试逻辑。比如在高并发计数场景,如果只调用一次incrementAndGet()而不配合循环,本质仍是单次尝试,失败就丢数据。
- 必须用循环包裹CAS操作:读取当前值→计算新值→调用
compareAndSet(old, new)→失败则重读重算 -
AtomicInteger.updateAndGet()和accumulateAndGet()已封装了循环逻辑,比手写while更安全 - 不要在CAS操作里做耗时任务(如IO、锁等待),否则自旋成本爆炸,可能引发CPU飙高
为什么ABA问题会让CAS失效
CAS只比对值是否相等,不关心中间变化过程。比如线程A读到值为100,被挂起;线程B把100→200→100改了两轮,A恢复后仍认为“没变过”,于是成功写入——但业务上这很可能已是脏状态。
典型场景是对象池、连接池的节点复用:一个被回收又重新分配的节点,地址相同、字段看似一致,但内部状态(如缓冲区内容、回调队列)早已不同。
立即学习“Java免费学习笔记(深入)”;
- Java提供
AtomicStampedReference解决:用一个int版本号配合引用,CAS同时校验引用+版本号 - 注意
stamp不是时间戳,要由业务主动递增,不能重复使用同一stamp值 - 若仅需避免ABA且不关心具体变化次数,用
AtomicMarkableReference(布尔标记)开销更小
LongAdder比AtomicLong更适合高并发计数
当多个线程频繁更新同一个计数器时,AtomicLong的CAS会因激烈竞争导致大量自旋失败,吞吐量骤降。LongAdder通过分段累加(cell数组)把竞争分散,最后再汇总,性能提升常达数倍。
但它不是“强一致”的:sum()返回的是近似实时值,中间可能有微小延迟;而longValue()和sum()行为一致,别误以为后者更“准确”。
- 适用场景明确:只做累加/统计,不依赖严格实时读取——如QPS统计、埋点计数
- 不适用于需要“读-改-写”原子性的逻辑(比如余额扣减必须基于当前精确值)
- 创建后不可序列化,若需持久化请用
sum()结果而非对象本身
自旋锁写不好反而比synchronized更慢
很多人以为“无锁=更快”,但纯CAS自旋在竞争激烈时会持续占用CPU,尤其在单核或线程数远超CPU核心数时,空转耗电、挤占调度资源,实际响应反而更差。
真实项目里,除非明确压测证明CAS路径显著优于锁(比如热点极窄、平均自旋1–2次就成功),否则优先用JVM优化后的synchronized(偏向锁→轻量锁→重量锁自动升级)。
- 自旋应设上限:比如最多重试100次,之后退化为
Thread.yield()或短暂LockSupport.parkNanos(1) - 避免在非CPU密集型场景滥用:比如网络请求后更新状态,等待IO的时间远大于CAS开销,此时锁更合理
- JDK9+的
VarHandle提供了更底层、可预测的CAS能力,但调试难度高,一般业务无需替换Atomic类
真正难的从来不是写出CAS循环,而是判断“此刻该不该用”以及“冲突概率到底多高”——这两点没法靠代码模板解决,得看监控里的重试率和CPU profile。










