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

前言

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

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

悲观锁和乐观锁其实本质都是一种思想,在JAVA中对于悲观锁的实现大家可能都很了解,可以通过synchronizedReentrantLock加锁实现,本文不展开讲解了。那么乐观锁在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乐观锁实现的原子类,比如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,所以线程不会陷入阻塞,这是效率提升的因素之一
    • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

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

乐观锁思想在JAVA中的实现——CAS的更多相关文章

  1. thinkphp继承高级model后的乐观锁运用(测试中)

    <?php class IndexAction extends Action { private $d_user; private $user; private $arr; public fun ...

  2. Java 中15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁,乐观锁,分段锁,自旋锁等等

    Java 中15种锁的介绍 Java 中15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁,乐观锁,分段锁,自旋锁等等,在读很多并发文章中,会提及各种各样锁如公平锁,乐观锁等等,这篇文章介绍各种锁的分类 ...

  3. Java 多线程之悲观锁与乐观锁

    一.悲观锁 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源 ...

  4. Java并发问题--乐观锁与悲观锁以及乐观锁的一种实现方式-CAS

    首先介绍一些乐观锁与悲观锁: 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁.传统的关系型数据库里边就用到了很 ...

  5. java多线程----悲观锁与乐观锁

    java多线程中悲观锁与乐观锁思想 一.悲观锁 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线 ...

  6. java笔记 -- 乐观锁与悲观锁

    何谓乐观锁和悲观锁 乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,悲观锁对应于生活中悲观的人总是想着事情往坏的方向发展.这两种人各有优缺点,不能不以场景而定说一种人好于另外一种人. 悲观锁 - ...

  7. java多线程系列3:悲观锁和乐观锁

    1.悲观锁和乐观锁的基本概念 悲观锁: 总是认为当前想要获取的资源存在竞争(很悲观的想法),因此获取资源后会立刻加锁,于是其他线程想要获取该资源的时候就会一直阻塞直到能够获取到锁: 在传统的关系型数据 ...

  8. 全面了解Java中的15种锁概念及机制!

    在读很多并发文章中,会提及各种各样锁如公平锁,乐观锁等等,这篇文章介绍各种锁的分类.介绍的内容如下: 1.公平锁 / 非公平锁 2.可重入锁 / 不可重入锁 3.独享锁 / 共享锁 4.互斥锁 / 读 ...

  9. Java中15种锁的介绍

    作者:搜云库技术团队 原文:https://segmentfault.com/a/1190000017766364 1. Java 中15种锁的介绍 在读很多并发文章中,会提及各种各样锁如公平锁,乐观 ...

  10. 深入理解Java中的锁

    转载:https://www.jianshu.com/p/2eb5ad8da4dc Java中的锁 常见的锁有synchronized.volatile.偏向锁.轻量级锁.重量级锁 1.synchro ...

随机推荐

  1. ProxySQL(13):ProxySQL集群

    文章转载自:https://www.cnblogs.com/f-ck-need-u/p/9362822.html ProxySQL有原生的集群功能,但是这个原生的集群功能还正在试验阶段.本文会详细介绍 ...

  2. 使用KVM的命令行方式安装centos7虚拟机

    前提条件 1.宿主机上已经安装KVM软件,参考网址:https://www.cnblogs.com/sanduzxcvbnm/p/15538881.html 2.已经上传centos7镜像到宿主机里 ...

  3. 使用docker方式安装wordpress

    mkdir -p /home/my_wordpress cd my_wordpress/ vim docker-compose.yml version: '3.3' services: db: ima ...

  4. 某云负载均衡获取客户端真实IP的问题

    某云负载均衡真实IP的问题,我们这边已经遇到过两次了.而且每次和售后沟通的时候都大费周折,主要是要给售后说明白目前文档的获取真实IP是有问题的,他们觉得文档上说明的肯定没问题,售后要是不明白,他们不会 ...

  5. 中小制造企业需要ERP和MES吗?

    并不是所有中小制造企业都需要ERP和MES,这个取决于你的规模和管理思维与模式!匹配很重要,不同规模的企业做不同的选择!比如你大型企业,一般是要使用ERP的,其工厂也应该需要上MES系统,ERP主要用 ...

  6. P7476 苦涩 题解

    Link 一道很好的复杂度均摊题目. 只需要考虑删除操作时的时间复杂度.保证复杂度的重点之一是精确定位到所有包含最大值的区间,即不去碰多余的区间.每次删除操作会删除若干个整个区间,以及至多两个区间被删 ...

  7. 自主创建mybtis管理应用,用以横向管理数据源

    这个是我写的第一个随手小记,一晃眼做后端开发也有7年多了,现在也准备将一些杂七杂八的资料整理下.也算是回顾这7年中做的比较有意思的东西了. 这个需求是我17年做的,当时的应用场景是仓储库比较多,随时会 ...

  8. 后端框架的学习----mybatis框架(6、日志)

    六.日志 如果一个数据库操作,出现了异常,我们需要排错,日志就是最好的帮手 setting设置 <settings> <setting name="logImpl" ...

  9. 成功解决IDEA中建立新项目Maven会默认选择配置(图解详细说明)

    文章目录 1.File-->Other Settings --> Settings for New Projects 2.选择自己的maven配置 3.效果展示(我这里省略建立新项目的过程 ...

  10. 10.-ORM-基础字段及选项

    一.ORM-基础字段及选项 任何关于表结构的修改,务必在对应模型类上修改 例如 给 books_book表添加一个info字段 varchar(100) 1.在模型中添加对应的类属性 2.执行数据库迁 ...