CAS原子操作

原子操作

所谓原子操作是指不会被线程调度机制打断的操作,当某次操作一旦开始,就一直运行到结束,中间不会有任何中断。
举个例子:

A想要从自己的帐户中转1000块钱到B的帐户里。那个从A开始转帐,到转帐结束的这一个过程,称之为一个事务。在这个事务里,要做如下操作:

  1. 从A的帐户中减去1000块钱。如果A的帐户原来有3000块钱,现在就变成2000块钱了。
  2. 在B的帐户里加1000块钱。如果B的帐户如果原来有2000块钱,现在则变成3000块钱了。
    如果在A的帐户已经减去了1000块钱的时候,忽然发生了意外,比如停电什么的,导致转帐事务意外终止了,而此时B的帐户里还没有增加1000块钱。那么,我们称这个操作失败了,要进行回滚。回滚就是回到事务开始之前的状态,也就是回到A的帐户还没减1000块的状态,B的帐户的原来的状态。此时A的帐户仍然有3000块,B的帐户仍然有2000块。

我们把这种要么一起成功(A帐户成功减少1000,同时B帐户成功增加1000),要么一起失败(A帐户回到原来状态,B帐户也回到原来状态)的操作叫原子性操作。

如果把一个事务可看作是一个程序,它要么完整的被执行,要么完全不执行。这种特性就叫原子性。

CAS

Compare And Set(或Compare And Swap),CAS是解决多线程并行情况下使用锁造成性能损耗的一种机制,采用这种无锁的原子操作可以实现线程安全,避免加锁的笨重性。
CAS操作包含三个操作数:内存位置(V)、预期原值(A)、新值(B)。
如果内存位置的值与预期原值相等,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。
无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”
在java中可以通过循环CAS的方式来实现原子操作。

CAS是实现自旋锁的基础,CAS 利用 CPU 指令保证了操作的原子性,以达到锁的效果,循环这个指令,直到成功为止。
java提供的CAS原子操作类AtomicInteger等,核心就是CAS(CompareAndSwap)。

注意:原子操作和锁是一样的一种可以保证线程安全的方式,如何让线程安全就看如何使用锁或者如何使用原子操作CAS使用了正确的原子操作,所以保证了线程安全。

原子操作类

当高并发的情况下,对于基本数据类型或者引用数据类型的操作,可能会产生线程安全问题,为了避免多线程问题的处理方式一般有加锁,但是加锁会影响性能,所以这个时候可以考虑使用原子操作类。CAS由于是在硬件方面保证的原子性,不会锁住当前线程,所以执行效率是很高的。
常见的原子操作类:
在这里插入图片描述

实战

1,多线程累加一个较大的数值,比较原子操作类和加锁各自的耗时。

public class AtomicIntegerDemo {
    
    static AtomicInteger atomicInteger = new AtomicInteger(1);

    public static void main(String[] args) throws InterruptedException {
//        test();
        Synch synch = new Synch();
        synch.start();
        synch.join();
        atomic();

//        System.out.println(add());
    }

    //使用加锁累加
    static class Synch extends Thread {
        @Override
        public void run() {
            //使用原子操作类统计
            long startTime = System.currentTimeMillis();
            int count = 1;
//            synchronized (this) {
//                for (int i = 0; i < 1000000000; i++) {
//                    count++;
//                }
//            }
            for (int i = 0; i < 1000000000; i++) {
                synchronized (this) {
                    count++;
                }
            }
            long endTime = System.currentTimeMillis();
            System.out.println("使用锁累加花费的时间:" + (endTime - startTime) + "......count = " + count);
        }
    }


    //使用原子操作类统计
    private static void atomic() {
        long startTime = System.currentTimeMillis();
        AtomicInteger atomicInteger = new AtomicInteger(1);
        for (int i = 0; i < 1000000000; i++) {
            atomicInteger.incrementAndGet();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("原子操作类累加花费的时间:" + (endTime - startTime) + "......count = " + atomicInteger.get());
    }

    //测试基本用法
    private static void test() {
        AtomicInteger num = new AtomicInteger(1);

        //++i
        System.out.println(num.incrementAndGet());

        //i++
        System.out.println(num.getAndIncrement());

        //CAS
        System.out.println(num.compareAndSet(3, 4));
        System.out.println(num.get());
    }

    //实现自定义的原子递增方法
    private static int add() {
        for (; ; ) {
            int i = atomicInteger.get();
            boolean b = atomicInteger.compareAndSet(i, i + 1);
            if (b) {
                return atomicInteger.get();
            }
        }
    }
}

运行结果:
在这里插入图片描述

2,使用AtomicInteger类的get方法和compareAndSet方法实现它的递增方法。

public class HalfAtomicInt {
    private AtomicInteger atomicI = new AtomicInteger(0);

    public void increament() {
        for (;;) {
            int i = atomicI.get();
            boolean suc = atomicI.compareAndSet(i, ++i);
            if (suc) {
                break;
            }
        }
    }
    
    public int getCount() {
    	return atomicI.get();
    }

}

总结

好处:保证了数据的原子性,避免线程安全问题,替代加锁的性能消耗。
坏处:
1,ABA问题(并发1在修改数据时,虽然还是A,但已经不是初始条件的A了,中间发生了A变B,B又变A的变化,此A已经非彼A,数据却成功修改,可能导致错误,这就是CAS引发的所谓的ABA问题。 可以使用乐观锁的方式解决。)
2,循环时间长开销大(自旋)
3,只能保证一个共享变量的原子操作(可以使用AtomicRefrence原子操作类将多个变量合并成一个对象来解决)
解决ABA问题:
AtomicMarkableReference:内部是一个boolean类型的版本号,可以记录是否被更改过。
AtomicStampedReference:内部是一个int类型的版本号,可以记录被更改的次数。

例如:使用AtomicStampedReference,避免ABA问题,查看内部是int类型的版本号。

public class AtomicStampedReferenceDemo {

    public static void main(String[] args) throws InterruptedException {
        //设置初始化版本号是0
        AtomicStampedReference<String> atomicStampedReference = new AtomicStampedReference("a1", 0);

        //初始的值和版本号
        String reference = atomicStampedReference.getReference();
        int stamp = atomicStampedReference.getStamp();

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("目前的值:" + reference + "............版本号:" + stamp
                        + ",修改结果:" + atomicStampedReference.compareAndSet(reference, "a2", stamp, stamp + 1));
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                String reference = atomicStampedReference.getReference();
                System.out.println("目前的值:" + reference + "............版本号:" + atomicStampedReference.getStamp()
                        + ",修改结果:" + atomicStampedReference.compareAndSet(reference, "a2", stamp, stamp + 1));
            }
        });

        thread1.start();
        thread1.join();
        thread2.start();
        thread2.join();
    }

}

运行结果:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_28822933/article/details/83341633