CAS操作和原子类

CAS概念

CAS的概念:CAS包含3个操作数:内存值V、旧的预期值A、要修改的新值B。当且仅当预期值A与内存值V相等时,将内存值V修改为B,否则什么都不需要做。
这是一种算法,用于在多线程写时保证一个变量的原子性。通过CAS可以更容易的实现一种类似乐观锁的机制来保证操作对象的安全。

以例子来说明,如果有一个n++操作,
1、首先要从主内存中取到n到工作cache中,
2、然后n+1的值赋给这个n,写道cache中。
3、最后在从cache中将这个值写入到主内存中。
CAS则需要在写入地址前加一步比较期望值的操作,
1、读取未修改的内存中的V值n,存一个旧的期望值A;
2、将要修改的新值B赋给n;
3、比较旧的期望值A与现在内存中的值V是否相同;
4、如果相同,将B值写入内存;
5、如果不相同,重复1、2、3操作;

它是如何避免多线程间的问题的?
假设有且只有两个线程,同时尝试修改值i,进行i=i+2操作;
线程1先完成操作,比较期望值,发现i没有被改动,进行把i+2写到内存中;

线程2之后再尝试进行操作,比较期望值发现,i变成了i+2;此时进行一个新的期望值对比重新进行赋值操作,此时erci比较期望值发现都是i+2,赋予最终结果i+2+2;
这样就算是一次线程安全的;当然不是完全安全,也存在一些问题:

CAS的问题

CAS虽然很高效地解决了原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大,以及只能保证一个共享变量的原子操作。
1)ABA问题。因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。

ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

2)循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:第一,它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清(CPU Pipeline Flush),从而提高CPU的执行效率。

3)只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方
式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

原子类

对于Java中的多线程编程,如果想要保证原子性(原子性是指某个操作或者一系列操作要么都成功,要么都失败,不允许出现因中断而导致的部分成功或部分失败的情况。)除了使用加显示锁或者说Synchronized,另外还有一种是原子类,原子类大多基于CAS实现,以整数来说,相对于使用Synchronized修饰的int来说,使用AtomicInteger一般来说更具有效率;

AtomicInteger

以赋值为例,AtomicInteger通过volatile来保证其有序性和可见性,通过CAS来实现其原子性;

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

        //从jdk底层拿到这个对象;Unsafe对象可用于在任意内存地址读取和写入数据。该类是可以直接对内存进行相关操作的,甚至还可以通过汇编指令直接进行CPU的操作。因此 绝不能将其传递给不受信任的代码。
        private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
        private volatile int value;

		//构造方法,使用初始化value
        public AtomicInteger(int initialValue) {
    
    
            value = initialValue;
        }
        //赋值操作
        public final int getAndSet(int newValue) {
    
    
            //这个U指unsafe对象,value为volatile修饰的值;方法源码见下文
            return U.getAndSetInt(this, VALUE, newValue);
         }
         
         ...
         ...
         ...
    }

    //@HotSpotIntrinsicCandidate注释方法特定于 HotSpot 虚拟机。 
    @HotSpotIntrinsicCandidate
    public final int getAndSetInt(Object o, long offset, int newValue) {
    
    
        int v;
        //通过getIntVolatile方法通过内存偏移量获取对象最新的值,
       //然后调用cas方法,失败了就不断的重试获取对象新值然后CAS,直到cas成功为止
        do {
    
    
        
            v = getIntVolatile(o, offset);
            
        } while (!weakCompareAndSetInt(o, offset, v, newValue));
        return v;
    }

//可以看到上述cas操作底层为JDK的本地方法compareAndSetInt
@HotSpotIntrinsicCandidate
    public final boolean weakCompareAndSetInt(Object o, long offset,
                                              int expected,
                                              int x) {
    
    
        return compareAndSetInt(o, offset, expected, x);
    }

 @HotSpotIntrinsicCandidate
    public final native boolean compareAndSetInt(Object o, long offset,
                                                 int expected,
                                                 int x);

除了AtomicInteger用于处理int,还有一些其他原子类;

AtomicBoolean

提供了一种原子性地读写布尔类型变量,类似于AtomicInteger,借助这个类型可以方便的实现TryLock;

AtomicLong

提供了原子性操作long类型数据;

AtomicReference类

提供了对象引用的非阻塞原子性读写操作,并且提供了其他一些高级的用法;
非阻塞的方式可以让其高效解决一些线程安全问题;比如ConcurrentHashMap;
它主要适用用于一些乐观锁场景,在一个线程在写入内存新值时,其他线程也可以进行一个同步运行的操作,而如果加锁,其他线程由于无锁而被迫阻塞,而CAS可以保证其他线程在无锁的情况下也是一种运行态;

以上的这些原子类均使用了volatile关键字+CAS算法无锁的操作方式来确保共享数据在多线程操作下的线程安全性。

volatile关键字保证了线程间的可见性,当某线程操作了被volatile关键字修饰的变量,其他线程可以立即看到该共享变量的变化。
CAS算,是通过操作CPU指令来得到保证的。CAS算法提供了一种快速失败的方式,当某线程修改已经被改变的数据时会快速失败。
当CAS算法对共享数据操作失败时,因为有自旋算法的加持,我们对共享数据的更新终究会得到计算。

如前面介绍CAS问题时所说,还有一个AtomicStampedReference;
AtomicStampedReference在构建的时候需要一个类似于版本号的int类型变量stamped,每一次针对共享数据的变化都会导致该stamped的增加(stamped的自增维护需要应用程序自身去负责,AtomicStampedReference并不提供),因此就可以避免ABA问题的出现,

AtomicArray

原子类的数组操作;

AtomicIntegerFieldUpdater

将对象中的一些值进行原子化转化操作,可以使用Atomic xxx FieldUpdater这种类来快速操作;

 // 定义AtomicIntegerFieldUpdater,通过newUpdater方法创建
    AtomicIntegerFieldUpdater<Alex> updater =
    AtomicIntegerFieldUpdater.newUpdater(User.class, "id");
    // 实例化User
    User user1 = new User();
    // ③ 原子性操作User类中的id属性
    int result = updater.addAndGet(user1, 1);
    assert result == 1;

Guess you like

Origin blog.csdn.net/qq_44830792/article/details/121381445