乐观锁思想在JAVA中的实现——CAS
创始人
2024-03-19 16:53:57
0

前言

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

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

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

问题引入

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

public class AccountUnsafe {// 余额private Integer balance;public AccountUnsafe(Integer balance) {this.balance = balance;}@Overridepublic Integer getBalance() {return balance;}@Overridepublic void withdraw(Integer amount) {balance -= amount;}
}
复制代码
  • 账户类,withdraw()方法是取钱方法。
public static void main(String[] args) {// 账户10000元AccountUnsafe account = new AccountUnsafe(10000);List 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 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乐观锁实现的原子类,比如AtomicIntegerAtomicReference等等。我们用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,所以线程不会陷入阻塞,这是效率提升的因素之一
    • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

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

上一篇: 高中周记

下一篇: 初中的作文

相关内容

热门资讯

朋友的句子 关于朋友的句子15篇  无论在学习、工作或是生活中,大家都听说过或者使用过一些比较经典的句子吧,根据...
眼里有星星的唯美短句 关于眼里有星星的唯美短句  1、看,天空上,月亮的旁边,有一颗特别闪亮的星星。星星伴着月亮,月亮伴着...
读书的作用优美句子 读书的作用优美句子(精选50句)  在平日的学习、工作和生活里,大家都接触过很多优秀的句子吧,句子由...
教育随笔:生存与事业 教育随笔:生存与事业  我们每个人都在生存着,生活着,但并不是每个人都拥有自己的事业。应该说事业是高...
形容天气多变的句子   形容天气多变的句子  1、过了一会儿乌云散了许多,我走出庭园,正准备上街买东西。可那该死的,早不...
抖音上最火的签名句子 抖音上最火的签名句子(精选250句)  在平平淡淡的学习、工作、生活中,大家一定都接触过一些使用较为...
我的青春我做主-随笔写作 我的青春我做主-随笔写作  我的青春我做主_随笔写作  所有的结局都已写好  所有的泪水都已启程  ...
幼儿园大班安全随笔 幼儿园大班安全随笔幼儿园大班安全随笔安全教育是幼儿园教育永恒的话题,幼儿园是纵多幼儿集体生活的的场所...
小学二年级教育随笔   小学二年级是儿童形成各种习惯的最佳时期,在这一阶段重视培养良好的学习习惯,不仅直接影响学生的学习...
友谊如花,清香漫洒 友谊如花,清香漫洒  导语:随笔,顾名思义:随笔一记,是散文的一个分支,是议论文的一个变体,兼有议论...
描写春天的随笔作文 描写春天的随笔作文  春姑娘提着花篮,穿着彩色连衣裙,悄悄地来到了人间。以下是“春天的随笔作文”,希...
初中作文三月有感 初中作文三月有感  三月时节,天气渐渐变暖,同时天气也渐渐变得阴晴不定,时而艳阳高照,时而阴云密布,...
谈写诗心情随笔 谈写诗心情随笔  其实不单是写诗,所有的事情,也都如此,任一件事都是无所谓难无所谓不难,它们都统一的...
小班的孩子随笔散文 小班的孩子随笔散文  小班的孩子  半年的时光匆匆走过,似乎还没有来得及打个招呼,光阴便从我们身边悄...
心情随笔 心情随笔_750字  在日常学习和工作中,大家一定都接触过随笔吧?随笔是散文的一种,可以不受体裁的限...
张继写诗传随笔散文 张继写诗传随笔散文  张继是一名普通书生,在唐朝,所有书生的理想都是一样的:成为状元,光宗耀祖。但前...
老周散文随笔 老周散文随笔  三姐夫是我大爷的三姑爷,我习惯的称呼他老周,已经年过半百,看上去很乐观,尤其戴上眼镜...
剥豆子有感生活随笔 剥豆子有感生活随笔  人生,或多或少,或有或无,总会缺少一些。如:贫穷的人缺少富裕,愚蠢的人缺少智慧...
听课总结随笔 听课总结随笔  就昨天去上了《当代国际关系与中国战略》的的思政课,好像还挺有意思的:  对于外交:中...
罗曼罗兰名人传读后感-随笔作... 罗曼罗兰名人传读后感-随笔作文  罗曼·罗兰的一生都没有停止过创作和研究,这是他的宿命。以下是“罗曼...