首页IT科技乐观锁的使用场景(乐观锁思想在JAVA中的实现——CAS)

乐观锁的使用场景(乐观锁思想在JAVA中的实现——CAS)

时间2025-05-05 13:09:33分类IT科技浏览3331
导读:更多技术干活尽在个人公众号——JAVA旭阳 前言...

更多技术干活尽在个人公众号——JAVA旭阳

前言

生活中我们看待一个事物总有不同的态度         ,比如半瓶水                  ,悲观的人会觉得只有半瓶水了      ,而乐观的人则会认为还有半瓶水呢         。很多技术思想往往源于生活      ,因此在多个线程并发访问数据的时候                 ,有了悲观锁和乐观锁                  。

悲观锁认为这个数据肯定会被其他线程给修改了         ,那我就给它上锁    ,只能自己访问                ,要等我访问完            ,其他人才能访问  ,我上锁         、解锁都得花费我时间      。 乐观锁认为这个数据不会被修改               ,我就直接访问               ,当我发现数据真的修改了,那我也“礼貌的            ”让自己访问失败      。

悲观锁和乐观锁其实本质都是一种思想            ,在JAVA中对于悲观锁的实现大家可能都很了解                  ,可以通过synchronized                  、ReentrantLock加锁实现   ,本文不展开讲解了                 。那么乐观锁在JAVA中是如何实现的呢?底层的实现机制又是什么呢?

问题引入

我们用一个账户取钱的例子来说明乐观锁和悲观锁的问题         。

public class AccountUnsafe { // 余额 private Integer balance; public AccountUnsafe(Integer balance) { this.balance = balance; } @Override public Integer getBalance() { return balance; } @Override public void withdraw(Integer amount) { balance -= amount; } } 账户类         ,withdraw()方法是取钱方法    。 public static void main(String[] args) { // 账户10000元 AccountUnsafe account = new AccountUnsafe(10000); List<Thread> ts = new ArrayList<>(); long start = System.nanoTime(); // 1000个线程                  ,每次取10元 for (int i = 0; i < 1000; i++) { ts.add(new Thread(() -> { account.withdraw(10); })); } ts.forEach(Thread::start); ts.forEach(t -> { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); long end = System.nanoTime(); // 打印账户余额和花费时间 log.info("账户余额:{}, 花费时间: {}", account.getBalance(), (end-start)/1000_000 + " ms"); } 账户默认有10000元      ,1000个线程取钱      ,每次取10元                 ,最后账户应该还有多少钱呢?

运行结果:

运行结果显示余额还有150元         ,显然出现并发问题                。

原因分析:

原因也很简单    ,取钱方法withdraw()的操作balance -= amount;看着就一行代码                ,实际上会生成多条指令            ,如下图所示:

多个线程运行的时候会进行线程切换  ,导致这个操作不是原子性               ,所以不是线程安全的            。

悲观锁解决

最简单的方法               ,我想大家都能想到吧,给withdraw()方法加锁            ,保证同一时刻只有一个线程能够执行这个方法                  ,保证了原子性  。

通过synchronized关键字加锁               。

运行结果:

运行结果正常   ,但是花费时间稍微多了一点

乐观锁解决

关键来了         ,如果用乐观锁的思想在JAVA中该如何实现呢?

大致思路就是我默认不加任何锁                  ,我先把余额减掉10元      ,最后更新余额的时候      ,发现余额和我一开始不一样了                 ,我就丢弃当前更新操作         ,重新读取余额的值    ,直到更新成功               。

找啊找                ,最终发现JDK中的Unsafe方法提供了这样的方法compareAndSwapInt。

先获取老的余额oldBalance            ,计算出新的余额newBalance 调用 unsafe.compareAndSwapInt()方法  ,如果内存中余额属性的偏移量BALANCE_OFFSET对应的值等于老的余额               ,说明的确没有被其他线程访问修改过               ,我就大胆的更新为newBalance,退出方法 否则的话            ,我就要进入下一次循环                  ,重新获取余额计算            。

那么是如何获取unsafe呢?

静态方法中通过反射的方法获取   ,因为Unsafe类太底层了         ,它一般不建议程序员直接使用                  。

这个Unsafe类的名称并不是说线程不安全的意思                  ,只是这个类太底层了      ,不要乱用      ,对程序员来说不大安全   。

最后别忘了余额balance要加volatile修饰         。

主要为了保证可见性                 ,让线程能够获取到其他线程修改的结果                  。

运行结果:

余额也为0         ,正常    ,而且运行速度稍微快了一丢丢

完成代码:

@Slf4j(topic = "a.AccountCAS") public class AccountCAS { // 余额 private volatile int balance; // Unsafe对象 static final Unsafe unsafe; // balance 字段的偏移量 static final long BALANCE_OFFSET; static { try { Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); unsafe = (Unsafe) theUnsafe.get(null); // balance 属性在 AccountCAS 对象中的偏移量                ,用于 Unsafe 直接访问该属性 BALANCE_OFFSET = unsafe.objectFieldOffset(AccountCAS.class.getDeclaredField("balance")); } catch (NoSuchFieldException | IllegalAccessException e) { throw new Error(e); } } public AccountCAS(Integer balance) { this.balance = balance; } public int getBalance() { return balance; } public void withdraw(Integer amount) { // 自旋 while (true) { // 获取老的余额 int oldBalance = balance; // 获取新的余额 int newBalance = oldBalance - amount; // 更新余额            ,BALANCE_OFFSET表示balance属性的偏移量  , 返回true表示更新成功               , false更新失败               ,继续更新 if(unsafe.compareAndSwapInt(this, BALANCE_OFFSET, oldBalance, newBalance)) { return; } } } public static void main(String[] args) { // 账户10000元 AccountCAS account = new AccountCAS(10000); List<Thread> ts = new ArrayList<>(); long start = System.nanoTime(); // 1000个线程,每次取10元 for (int i = 0; i < 1000; i++) { ts.add(new Thread(() -> { account.withdraw(10); })); } ts.forEach(Thread::start); ts.forEach(t -> { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); long end = System.nanoTime(); // 打印账户余额和花费时间 log.info("账户余额:{}, 花费时间: {}", account.getBalance(), (end-start)/1000_000 + " ms"); } }

乐观锁改进

好麻烦呀            ,我们自己调用原生的UnSafe类实现乐观锁                  ,有什么更好的方式吗?

当然有   ,其实JDK给我们封装了很多基于UnSafe乐观锁实现的原子类         ,比如AtomicInteger      、AtomicReference等等      。我们用AtomicInteger改写下上面的实现      。

使用JDK中的原子类AtomicInteger作为余额的类型 取钱逻辑直接调用addAndGet方法

运行结果:

原理:

查看源码最终也是调用的Unsafe方法                 。

CAS机制

前面的一个取钱的例子                  ,大家是不是对乐观锁的思想以及在JAVA中的实现更深入的认识         。

在JAVA中对这种实现起了一个名字      ,叫做CAS, 全称Compare And Swap      ,是不是很形象                 ,先比较         ,然后再替换    。

那CAS的本质是什么?

CAS先比较然后再替换    ,感觉是有2步                ,比较和替换            ,不像是原子性操作  ,如果不是原子性操作问题就可大了                。实际上               ,CAS本质对应的是一条指令               ,是原子操作            。

CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性  。

强调一点            ,CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果                  ,因为volatile会保证变量的可见性               。

总结

结合 CAS 和 volatile 可以实现无锁并发   ,适用于线程数少      、多核 CPU 的场景或者读多写少的场景               。

CAS 是基于乐观锁的思想:最乐观的估计         ,不怕别的线程来修改共享变量                  ,就算改了也没关系      ,我吃亏点再重试呗。 synchronized 是基于悲观锁的思想:最悲观的估计      ,得防着其它线程来修改共享变量                 ,我上了锁你们都别想改         ,我改完了解开锁    ,你们才有机会            。 CAS 体现的是无锁并发                 、无阻塞并发                ,请仔细体会这两句话的意思 因为没有使用 synchronized            ,所以线程不会陷入阻塞  ,这是效率提升的因素之一 但如果竞争激烈               ,可以想到重试必然频繁发生               ,反而效率会受影响

如果本文对你有帮助的话,请留下一个赞吧

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
药物伤害大吗(Using YQL as crawler for Javascript) seo的关键是什么(seo的根本作用)