转载自 乐观锁的一种实现方式——CAS

在深入理解乐观锁与悲观锁一文中我们介绍过锁。本文在这篇文章的基础上,深入分析一下乐观锁的实现机制,介绍什么是CAS、CAS的应用以及CAS存在的问题等。

线程安全

众所周知,Java是多线程的。但是,Java对多线程的支持其实是一把双刃剑。一旦涉及到多个线程操作共享资源的情况时,处理不好就可能产生线程安全问题。线程安全性可能是非常复杂的,在没有充足的同步的情况下,多个线程中的操作执行顺序是不可预测的。

Java里面进行多线程通信的主要方式就是共享内存的方式,共享内存主要的关注点有两个:可见性和有序性。加上复合操作的原子性,我们可以认为Java的线程安全性问题主要关注点有3个:可见性、有序性和原子性。

Java内存模型(JMM)解决了可见性和有序性的问题,而锁解决了原子性的问题。这里不再详细介绍JMM及锁的其他相关知识。但是我们要讨论一个问题,那就是锁到底是不是有利无弊的?

锁存在的问题

Java在JDK1.5之前都是靠synchronized关键字保证同步的,这种通过使用一致的锁定协议来协调对共享状态的访问,可以确保无论哪个线程持有共享变量的锁,都采用独占的方式来访问这些变量。独占锁其实就是一种悲观锁,所以可以说synchronized是悲观锁。

悲观锁机制存在以下问题:

在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。

一个线程持有锁会导致其它所有需要此锁的线程挂起。

如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

与锁相比,volatile变量是一个更轻量级的同步机制,因为在使用这些变量时不会发生上下文切换和线程调度等操作,但是volatile不能解决原子性问题,因此当一个变量依赖旧值时就不能使用volatile变量。因此对于同步最终还是要回到锁机制上来。

乐观锁

乐观锁( Optimistic Locking)其实是一种思想。相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。

上面提到的乐观锁的概念中其实已经阐述了他的具体实现细节:主要就是两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是Compare and Swap(CAS)。

CAS

CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”这其实和乐观锁的冲突检查+数据更新的原理是一样的。

这里再强调一下,乐观锁是一种思想。CAS是这种思想的一种实现方式。

Java对CAS的支持

在JDK1.5 中新增java.util.concurrent(J.U.C)就是建立在CAS之上的。相对于对于synchronized这种阻塞算法,CAS是非阻塞算法的一种常见实现。所以J.U.C在性能上有了很大的提升。

我们以java.util.concurrent中的AtomicInteger为例,看一下在不使用锁的情况下是如何保证线程安全的。主要理解getAndIncrement方法,该方法的作用相当于 ++i 操作。

public class AtomicInteger extends Number implements java.io.Serializable {  

       private volatile int value;  

   public final int get() {         return value;     }  

   public final int getAndIncrement() {         for (;;) {             int current = get();             int next = current + 1;             if (compareAndSet(current, next))                 return current;         }     }  

   public final boolean compareAndSet(int expect, int update) {         return unsafe.compareAndSwapInt(this, valueOffset, expect, update);     }  }

在没有锁的机制下需要字段value要借助volatile原语,保证线程间的数据是可见的。这样在获取变量的值的时候才能直接读取。然后来看看++i是怎么做到的。

getAndIncrement采用了CAS操作,每次从内存中读取数据然后将此数据和+1后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止。而compareAndSet利用JNI来完成CPU指令的操作。

ABA问题

CAS会导致“ABA问题”。

CAS算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。

比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。

部分乐观锁的实现是通过版本号(version)的方式来解决ABA问题,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。

总结

Java中的线程安全问题至关重要,要想保证线程安全,就需要锁机制。锁机制包含两种:乐观锁与悲观锁。悲观锁是独占锁,阻塞锁。乐观锁是非独占锁,非阻塞锁。有一种乐观锁的实现方式就是CAS ,这种算法在JDK 1.5中引入的java.util.concurrent中有广泛应用。但是值得注意的是这种算法会存在ABA问题。

CAS与对象创建

另外,CAS还有一个应用,那就是在JVM创建对象的过程中。对象创建在虚拟机中是非常频繁的。即使是仅仅修改一个指针所指向的位置,在并发情况下也不是线程安全的,可能正在给对象A分配内存空间,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题的方案有两种,其中一种就是采用CAS配上失败重试的方式保证更新操作的原子性。

乐观锁的一种实现方式——CAS相关推荐

  1. 悲观锁和乐观锁_乐观锁和悲观锁 以及 乐观锁的一种实现方式-CAS

    悲观锁 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞知道它拿到锁.传统的关系型数据库里面就用到了很多的这种锁机制,比如行锁,表锁等 ...

  2. 乐观锁与悲观锁以及乐观锁的一种实现方式-CAS

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

  3. Java并发 乐观锁和悲观锁 乐观锁的一种实现方式CAS

    为什么80%的码农都做不了架构师?>>>    首先介绍一些乐观锁与悲观锁: 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人 ...

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

    Java并发问题–乐观锁与悲观锁以及乐观锁的一种实现方式-CAS </h1><div class="clear"></div><div c ...

  5. java学习系列2(并发锁问题-乐观锁与悲观锁以及乐观锁的一种实现方式-CAS)

    Java并发问题--乐观锁与悲观锁以及乐观锁的一种实现方式-CAS 首先介绍一些乐观锁与悲观锁: 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别 ...

  6. 乐观锁的两种实现方式

    什么场景下需要使用锁? 在多节点部署或者多线程执行时,同一个时间可能有多个线程更新相同数据,产生冲突,这就是并发问题.这样的情况下会出现以下问题: 更新丢失:一个事务更新数据后,被另一个更新数据的事务 ...

  7. 为什么多个线程不可能同时抢到一把锁_HFL Redis_12_redis分布式锁的3种实现方式...

    HotFrameLearning(简称 HFL) Redis_12_redis分布式锁的3种实现方式 - 一.大致介绍 ``` 1.昨天介绍完redis的数据结构后,有小伙伴让本人讲讲redis的分布 ...

  8. memcached 分布式锁 java_分布式锁的三种实现方式

    分布式锁的三种实现方式 一.zookeeper 1.实现原理: 基于zookeeper瞬时有序节点实现的分布式锁,其主要逻辑如下(该图来自于IBM网站).大致思想即为:每个客户端对某个功能加锁时,在z ...

  9. 分布式锁以及三种加锁方式

    在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务.分布式锁等.那具体什么是分布式锁,分布式锁应用在哪些业务场景.如何来实现分布式锁呢? 一 为什么要使用分布式锁 我 ...

最新文章

  1. PHP用户输入安全过滤和注入攻击检测
  2. Effective Java 学习笔记 1
  3. 作者:陶克(1988-),男,博士,北京系统工程研究所助理研究员。
  4. EDA实验课课程笔记(二)——Vim编辑器
  5. 【Scheme归纳】4 高阶函数
  6. 套接口编程理论基础:正常启动
  7. 在SOLIDWORKS Electrical中,如何创建电气原理图符号库?
  8. centOS 8 操作系统下载与安装
  9. mfc入门基础(三)创建对话框
  10. 电脑开机没反应如何解决?
  11. shader篇-处理复杂光照
  12. 1988年图灵奖--伊万·萨瑟兰简介
  13. 【vue js】省市区js数据转为tree结构
  14. python中arcsec_主页
  15. Unity动画☀️7. 通过MatchTarget匹配跳墙动画、Vector3、获取动画层GetCurrentAnimatorStateInfo(a).IsName(b)
  16. 前端技能树,面试复习第 1 天—— 建立前端知识框架 | HTML 知识考察点
  17. i12蓝牙耳机充电仓怎么看充满电_平价好声音,百元真无线蓝牙耳机——西圣io7...
  18. 深搜、广搜、搜索剪枝
  19. 数据结构--数组和广义表
  20. 机器学习(二)非参数估计matlab例程

热门文章

  1. libgo 支持mysql,loadrunner通过使用libmysql.dll完成mysql的测试-Go语言中文社区
  2. x12arima季节调整方法_《时间序列X-12-ARIMA季节调整:原理与方法》
  3. Bound Found POJ - 2566(尺取法+前缀和创造区间变化趋势)
  4. 网络原理往期考试题+部分详解+最终版
  5. UVA10129 Play on Words (并查集判连通+欧拉回路)
  6. linux aspnet服务器,在Linux中安装ASPNET.Core3.0运行时的示例代码
  7. java 持续交付_【Java架构:持续交付】一篇文章搞掂:Jenkins
  8. python数据类型及使用方法_python 基本数据类型及方法
  9. Manthan, Codefest 19 (open for everyone, rated, Div. 1 + Div. 2) F. Bits And Pieces sosdp预处理超集
  10. Educational Codeforces Round 101 (Rated for Div. 2) D. Ceil Divisions 思维 + 根号数