java---CAS原理分析详解

目录

一、什么是CAS

二、乐观锁与悲观锁

1.乐观锁出现原因

2.乐观锁

3.乐观锁的实现机制---CAS

三、JAVA对CAS的支持

首先演示实际的操作

 上述过程的内部原理(java层面)

四、CAS缺陷

1.ABA问题

解决ABA问题

2.循环时间长开销大

3.只能保证一个变量的原子操作

4.解决方式

总结


一、什么是CAS

CAS的全称为Compare-And-Swap ,它是一条CPU同步原语,是一种硬件对并发的支持。它的功能是判断内存某个位置的值是否为预期值,如果是则更新为新的值,这个过程是原子的。

CAS并发原语体现在Java语言中就是sun.misc.UnSafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现CAS汇编指令。这是一种完全依赖于硬件功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许中断,也就是说CAS是一条原子指令,不会造成所谓的数据不一致的问题。

CAS加volatile关键字是实现并发包的基石。没有CAS就不会有并发包,synchronized是一种独占锁、悲观锁,java.util.concurrent中借助了CAS指令实现了一种区别于synchronized的一种乐观锁。


 

二、乐观锁与悲观锁

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样当第二个线程想拿这个数据的时候,第二个线程会一直堵塞,直到第一个释放锁,他拿到锁后才可以访问。传统的数据库里面就用到了这种锁机制,例如:行锁,表锁,读锁,写锁,都是在操作前先上锁。java中的synchronized的实现也是一种悲观锁。

乐观锁:乐观锁概念为,每次拿数据的时候都认为别的线程不会修改这个数据,所以不会上锁,但是在更新的时候会判断一下在此期间别的线程有没有修改过数据,乐观锁适用于读操作多的场景,这样可以提高程序的吞吐量。在Java中java.util.concurrent.atomic包下面的原子变量就是使用了乐观锁的一种实现方式CAS实现。

1.乐观锁出现原因

java在1.5之前都是靠synchronized关键字保证同步,synchronized保证了无论哪个线程持有共享变量的锁,都会采用独占的方式来访问这些变量。这种情况下:

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

2.如果一个线程持有锁,其他的线程就都会挂起,等待持有锁的线程释放锁。

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

对比于悲观锁的这些问题,另一个更加有效的锁就是乐观锁。 乐观锁就是:每次不加锁而是假设没有并发冲突去操作同一变量,如果有并发冲突导致失败,则重试直至成功。

2.乐观锁

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

3.乐观锁的实现机制---CAS

乐观锁是一种思想,CAS只是这种思想的一种实现方式。

乐观锁主要就是两个步骤:冲突检测和数据更新。当多个线程尝试使用CAS同时更新同一个变量时,只有一个线程可以更新变量的值,其他的线程都会失败,失败的线程并不会挂起,而是告知这次竞争中失败了,并可以再次尝试。

CAS操作包括三个操作数:需要读写的内存位置(V)、预期原值(A)、新值(B)。如果内存位置与预期原值的A相匹配,那么将内存位置的值更新为新值B。如果内存位置与预期原值的值不匹配,那么处理器不会做任何操作。

三、JAVA对CAS的支持

下面通过看下并发包中的原子操作类AtomicInteger来看下,如何在不使用锁的情况下保证线程安全,主要看下getAndIncrement方法,相当于i++的操作:

首先演示实际的操作

public class Test2 {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(45);
        //返回的是原来的值
        int andIncrement = atomicInteger.getAndIncrement();
        System.out.println(andIncrement);  //输出45
        System.out.println(atomicInteger);  //输出计算后的值46
    }
}

运行结果:

 

 上述过程的内部原理(java层面)

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

 // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

      static {
        try {
              //  返回指定的变量value在所属类中的内存偏移地址
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }


     //保证了内存可见性
     private volatile int value;
        
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

}

下面就是Unsafe类中的方法

 // var1表示这个unsafe对象的地址
 // var2表示返回指定的变量在所属类中的内存偏移地址
 //var4表示要加的数,所以在这里为1
  //var5表示读到对应地址的值
 public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
      
       //自旋
        do {
            
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);


public native int getIntVolatile(Object var1, long var2);

首先value使用了volatile修饰,这也就保证了他的可见性与有序性。

四、CAS缺陷

1.ABA问题

假设内存中有一个值为A的变量,存储在地址V中

此时有三个线程想使用CAS的方式更新这个变量的值,每个线程的执行时间有略微偏差。线程1和线程2已经获取当前值,线程3还未获取当前值。

 

接下来,线程1先一步执行成功,把当前值成功从A更新为B;同时线程2因为某种原因被阻塞住,没有做更新操作;线程3在线程1更新之后,获取了当前值B。

 

在之后,线程2仍然处于阻塞状态,线程3继续执行,成功把当前值从B更新成了A。

 

最后,线程2终于恢复了运行状态,由于阻塞之前已经获得了“当前值A”,并且经过compare检测,内存地址V中的实际值也是A,所以成功把变量值A更新成了B。

 

上述就是ABA问题,他的问题在于此时获取的A值已经不是原来的A值了,会对结果有很大影响。 

解决ABA问题

真正要做到严谨的CAS机制,我们在compare阶段不仅要比较期望值A和地址V中的实际值,还要比较变量的版本号是否一致。 

我们仍然以刚才的例子来说明,假设地址V中存储着变量值A,当前版本号是01。线程1获取了当前值A和版本号01,想要更新为B,但是被阻塞了。

 这时候,内存地址V中变量发生了多次改变,版本号提升为03,但是变量值仍然是A。 

 

随后线程1恢复运行,进行compare操作。经过比较,线程1所获得的值和地址的实际值都是A,但是版本号不相等,所以这一次更新失败。

在Java中,AtomicStampedReference类就实现了用版本号作比较额CAS机制。

2.循环时间长开销大

自旋CAS如果一直循环执行,一直不成功,会给CPU带来非常大的执行开销。很多时候,CAS思想体现,是有个自旋次数的,就是为了避开这个耗时问题~

这里注意一下子,如果只是单纯的使用compareAndSet(expect,update),内部只是调用了一次unsafe.compareAndSwapInt()方法,这是没有自旋的,返回的结果要么是true,要么是false。而对于上述讲的操作类AtomicInteger,它相当于是实现了自旋锁,如果没有就会一直循环 unsafe.compareAndSwapInt()方法。

3.只能保证一个变量的原子操作

CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。

4.解决方式

 1.使用互斥锁来保证原子性;

2.将多个变量封装成对象,通过AtomicReference来保证原子性。

内部的源码:

 private volatile V value;

就是使用泛型来接收多个变量的对象。 


总结

加油哦~~推荐小伙伴去看一看源码会更好的了解,源码也不是太难~~~

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java CAS(Compare And Swap,比较并交换)是一种常用于多线程编程的原子操作。其主要作用是在多线程环境下保证变量的原子性和一致性,解决多线程竞争条件下的并发问题。 Java CAS原理是通过比较内存的值与期望值是否相等来确定是否进行交换,其核心思想是利用硬件的原子性操作来实现并发的同步,而不需要使用锁(synchronized)等机制。 具体来说,CAS操作包含三个参数:内存地址、旧的预期值和新的值。 1. 首先,CAS会将当前内存地址的值与旧的预期值进行比较,如果相等,则说明内存的值未被其他线程修改。 2. 然后,CAS会使用新的值来更新内存地址的值,完成交换操作。 3. 最后,CAS会返回旧的预期值,可以通过返回值进行判断操作是否成功。 需要注意的是,CAS是一种乐观的并发控制方式,它不会阻塞线程,而是通过不断重试的方式来保证操作的原子性。如果CAS操作失败,那么线程会重新读取内存的值,并重新尝试进行CAS操作,直到成功为止。 然而,CAS也存在一些问题。首先,CAS需要频繁地读取和写入内存,这对内存带宽的要求较高;其次,由于CAS操作是无锁的,因此存在ABA问题,即在操作过程,如果其他线程修改了预期值两次并恢复为原来的值,CAS操作无法察觉。 总之,Java CAS作为一种基于硬件支持的原子操作,可以在多线程环境下实现高效的同步控制,然而它也需要开发人员自行处理ABA问题以及确保程序的正确性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值