温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

JAVA怎么实现乐观锁及CAS机制

发布时间:2022-12-05 09:29:53 来源:亿速云 阅读:111 作者:iii 栏目:开发技术

本篇内容介绍了“JAVA怎么实现乐观锁及CAS机制”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

    前言

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

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

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

    悲观锁和乐观锁其实本质都是一种思想,在JAVA中对于悲观锁的实现大家可能都很了解,可以通过synchronizedReentrantLock加锁实现。

    问题引入

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

    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元,最后账户应该还有多少钱呢?

    运行结果:

    JAVA怎么实现乐观锁及CAS机制

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

    原因分析:

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

    JAVA怎么实现乐观锁及CAS机制

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

    悲观锁解决

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

    JAVA怎么实现乐观锁及CAS机制

    通过synchronized关键字加锁。

    运行结果:

    JAVA怎么实现乐观锁及CAS机制

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

    乐观锁解决

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

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

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

    JAVA怎么实现乐观锁及CAS机制

    • 先获取老的余额oldBalance,计算出新的余额newBalance

    • 调用 unsafe.compareAndSwapInt()方法,如果内存中余额属性的偏移量BALANCE_OFFSET对应的值等于老的余额,说明的确没有被其他线程访问修改过,我就大胆的更新为newBalance,退出方法

    • 否则的话,我就要进入下一次循环,重新获取余额计算。

    那么是如何获取unsafe呢?

    JAVA怎么实现乐观锁及CAS机制

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

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

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

    JAVA怎么实现乐观锁及CAS机制

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

    运行结果:

    JAVA怎么实现乐观锁及CAS机制

    余额也为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乐观锁实现的原子类,比如AtomicIntegerAtomicReference等等。我们用AtomicInteger改写下上面的实现。

    JAVA怎么实现乐观锁及CAS机制

    • 使用JDK中的原子类AtomicInteger作为余额的类型

    • 取钱逻辑直接调用addAndGet方法

    运行结果:

    JAVA怎么实现乐观锁及CAS机制

    原理:

    JAVA怎么实现乐观锁及CAS机制

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

    CAS机制

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

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

    那CAS的本质是什么?

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

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

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

    “JAVA怎么实现乐观锁及CAS机制”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注亿速云网站,小编将为大家输出更多高质量的实用文章!

    向AI问一下细节

    免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

    AI