java多线程之-CAS无锁
1.背景
加锁确实能解决线程并发的的问题,但是会造成线程阻塞等待等问题
那么有没有一种方法,既可以线程安全,又不会造成线程阻塞呢?
答案是肯定的......请看如下案例
注意:重要的文字说明,写在了代码注释上,这样便于大家理解,请阅读代码和注释加以理解;
2.取钱案例引出问题
启动10000个线程,每个线程减去10元
原来账户共10000 0元
正常情况账户最后的余额应该是0元
测试多线程并发问题
先定义一个通用的接口,后面使用不同实现来测试
账户Money接口:

package com.ldp.demo05; import java.util.ArrayList; import java.util.List; /** * @author 姿势帝-博客园 * @address https://www.cnblogs.com/newAndHui/ * @WeChat 851298348 * @create 02/16 8:14 * @description */ public interface Money { // 获取余额 Integer getBalance(); // 取款 void reduce(Integer amount); /** * 启动10000个线程,每个线程减去10元 * 原来账户共10000 0元 * 正常情况应该是0元 * 测试多线程并发问题 * * @param account */ static void handel(Money account) { List<Thread> ts = new ArrayList<>(); long start = System.nanoTime(); for (int i = 0; i < 10000; i++) { ts.add(new Thread(() -> { account.reduce(10); })); } ts.forEach(Thread::start); ts.forEach(t -> { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); long end = System.nanoTime(); System.out.println("当前余额:" + account.getBalance() + " ,耗时: " + (end - start) / 1000_000 + " ms"); } }
2.1.存在线程安全的解决方案

package com.ldp.demo05.impl; import com.ldp.demo05.Money; /** * @author 姿势帝-博客园 * @address https://www.cnblogs.com/newAndHui/ * @WeChat 851298348 * @create 02/16 8:17 * @description */ public class UnSafeMoney implements Money { private Integer balance; public UnSafeMoney(Integer balance) { this.balance = balance; } @Override public Integer getBalance() { return balance; } @Override public void reduce(Integer amount) { // 存在线程不安全 balance -= amount; } }
2.2.使用传统锁的解决方案

package com.ldp.demo05.impl; import com.ldp.demo05.Money; /** * @author 姿势帝-博客园 * @address https://www.cnblogs.com/newAndHui/ * @WeChat 851298348 * @create 02/16 8:17 * @description */ public class SyncSafeMoney implements Money { private Integer balance; public SyncSafeMoney(Integer balance) { this.balance = balance; } @Override public Integer getBalance() { return balance; } @Override public synchronized void reduce(Integer amount) { // 当前对象加锁 安全 balance -= amount; } }
2.3.使用CAS无锁的解决方案

package com.ldp.demo05.impl; import com.ldp.demo05.Money; import java.util.concurrent.atomic.AtomicInteger; /** * @author 姿势帝-博客园 * @address https://www.cnblogs.com/newAndHui/ * @WeChat 851298348 * @create 02/16 8:30 * @description <p> * 无锁的思路 * * </p> */ public class CASSafeMoney implements Money { private AtomicInteger balance; public CASSafeMoney(Integer balance) { this.balance = new AtomicInteger(balance); } @Override public Integer getBalance() { return balance.get(); } /** * compareAndSet 做这个检查,在 set 前,先比较 prev 与当前值不一致了,next 作废,返回 false 表示失败 * <p> * 其实 CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性。 * 在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再 * 开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。 * <p> * CAS 的特点 * 结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。 * 1.CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,重试在执行【修改】。 * 2.synchronized 是基于悲观锁的思想:最悲观的估计,防着其它线程来修改共享变量,当前线程上锁后,其他线程阻塞等待。 * 3.CAS 体现的是【无锁并发、无阻塞并发】。 * 因为没有使用 synchronized,所以线程【不会陷入阻塞】,这是效率提升的因素之一, * 但如果竞争激烈,可以想到重试必然频繁发生,反而频繁切换上下文,效率会受影响。 * 4.特别注意: * 无锁情况下,但如果竞争激烈,因为线程要保持运行,需要CPU 的支持,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。 * * @param amount */ @Override public void reduce(Integer amount) { // 不断尝试直到成功为止 while (true) { // 修改前的值 int prev = balance.get(); // 修改后的值 int next = prev - amount; // 执行修改 compareAndSet 使用CAS乐观锁实现 if (balance.compareAndSet(prev, next)) { break; } } // 简要写法 // balance.addAndGet(-1 * amount); } }
3.测试

package com.ldp.demo05; import com.ldp.demo05.impl.CASSafeMoney; /** * @author 姿势帝-博客园 * @address https://www.cnblogs.com/newAndHui/ * @WeChat 851298348 * @create 02/16 8:20 * @description */ public class Test01Money { public static void main(String[] args) { // 1.无锁不安全 当前余额:29530 ,耗时: 4847 ms // Money.handel(new UnSafeMoney(100000)); // 2.synchronized加锁安全 当前余额:0 ,耗时: 7386 ms // Money.handel(new SyncSafeMoney(100000)); // 3.使用乐观锁 CAS 当前余额:0 ,耗时: 3466 ms Money.handel(new CASSafeMoney(100000)); } }
4.CompareAndSet 方法分析

package com.ldp.demo05; import com.common.MyThreadUtil; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.atomic.AtomicInteger; /** * @author 姿势帝-博客园 * @address https://www.cnblogs.com/newAndHui/ * @WeChat 851298348 * @create 02/16 8:49 * @description */ @Slf4j public class Test02CompareAndSet { /** * 观察多线程修改值 * * @param args */ public static void main(String[] args) { AtomicInteger n = new AtomicInteger(100); int mainPrev = n.get(); log.info("当前值:{}", n.get()); // 线程 t1 将其修改为 90 new Thread(() -> { // 模拟睡眠3秒 MyThreadUtil.sleep(1); boolean b = n.compareAndSet(mainPrev, 90); log.info("修改结果:{}", b); }, "t1").start(); // 模拟睡眠3秒 MyThreadUtil.sleep(2); new Thread(() -> { boolean b = n.compareAndSet(mainPrev, 80); log.info("修改结果:{}", b); }, "t2").start(); // 最后结果值 MyThreadUtil.sleep(2); log.info("最后值为={}", n.get()); // 21:04:15.369 [main] -> 当前值:100 // 21:04:16.451 [t1] -> 修改结果:true // 21:04:17.457 [t2] -> 修改结果:false // 21:04:19.457 [main] -> 最后值为=90 } }
完美!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
2021-02-19 maven项目打包时排除依赖包