文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2023-07-04 18:09

关注

本篇内容介绍了“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机制

那么是如何获取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机制

运行结果:

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机制”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注编程网网站,小编将为大家输出更多高质量的实用文章!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯