乐观锁思想在JAVA中的实现——CAS(java中乐观锁和悲观锁)

  本篇文章为你整理了乐观锁思想在JAVA中的实现——CAS(java中乐观锁和悲观锁)的详细内容,包含有java 乐观锁 例子 java中乐观锁和悲观锁 什么是乐观锁 乐观锁的业务场景及实现方式 乐观锁思想在JAVA中的实现——CAS,希望能帮助你了解 乐观锁思想在JAVA中的实现——CAS。

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

  悲观锁认为这个数据肯定会被其他线程给修改了,那我就给它上锁,只能自己访问,要等我访问完,其他人才能访问,我上锁、解锁都得花费我时间。

  乐观锁认为这个数据不会被修改,我就直接访问,当我发现数据真的修改了,那我也“礼貌的”让自己访问失败。

  悲观锁和乐观锁其实本质都是一种思想,在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,所以线程不会陷入阻塞,这是效率提升的因素之一

  但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

  以上就是乐观锁思想在JAVA中的实现——CAS(java中乐观锁和悲观锁)的详细内容,想要了解更多 乐观锁思想在JAVA中的实现——CAS的内容,请持续关注盛行IT软件开发工作室。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: