JAVA并发编程-3-原子操作CAS和原子类

上一章:看这里JAVA并发编程-2-线程并发工具类

一、CAS原理

1、为什么要有CAS

java中提供了sychronized和ReentrantLock的锁机制,这是一种阻塞同步,可以理解是为一种悲观锁的并发策略,总是认为只要不去做正确的同步措施,就肯定会出现问题,无论共享数据是否真的会出现竞争,都要进行加锁,需要进行线程等待唤醒等操作,性能消耗是很大的。

随着 硬件指令集 的发展,我们有了另外一个选择:基于冲突检测的乐观并发策略。通俗的讲就是先操作,如果没有其他线程竞争共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就采取其他补偿措施(最常见的补偿措施就是不断重试,直到成功为止),这种乐观的并发策略不需要把线程挂起,是一种非阻塞同步

2、通过硬件指令集来保证

上面这段话我们特别标注了 硬件指令集 ,CAS(Compare-and-Swap)就是通过硬件指令来保证的,硬件保证一个从语义上看起来需要多次的操作只通过一条指令就能完成。这类常用的指令有:

  • 测试并设置(Test-and-Set)
  • 获取并添加(Fetch-and-Increment)
  • 交换(Swap)
  • 比较并交换(Compare-and-Swap)

Compare-and-Swap是现代计算机新增的指令,java从JDK1.5开始支持cas指令,通过sun.misc.Unsafe类里面的方法包装提供,虚拟机在内部对方法做了特殊处理,即使编译的结果就是一条平台相关的处理器CAS指令,没有方法调用的过程,或者可以任务被无条件内联进去了。

3、什么是CAS

CAS是怎样的一个过程呢?

需要有3个操作数:
内存位置(可以理解为变量的内存地址) V
旧的预期值 A
新值 B

假设一个线程要更改内存位置V的值,会先去改地址值拿到原值A,随后将值改为B,并同步会内存位置V。在将新值同步回内存时,发生一次CAS,先比较(Comapre)现在地址V的值是不是旧的预期值A,如果是,就将新值B赋值到地址V处(swap),否则不会执行更新,但是无论是否更新了V值,都会返回V的旧值,旧值不符合预期就进行补偿策略,即在发生一次上述过程。

二、CAS的问题

1、ABA问题

CAS从语义上来说就不尽完美,存在这样一个逻辑漏洞:如果一个变量V初次读取时是A值,后面准备赋值时它仍然是A值,那么我们就能说它的值没有被其他线程改变过吗?如果这段时间内它的值被改成了B,后来又被回了A,那CAS就误认为他从来没有改变过,这个就是CAS的ABA问题。

JUC包为了解决这个问题提供了一个带标记的原子应用类“AtomicStampedReference”,可以通过控制变量值的版本来保证CAS的正确性。目前来看这个类比较鸡肋,大部分情况下ABA问题并不会影响程序并发的正确性,如果需要解决ABA问题,改用锁的方式获取会更好。

2、开销问题

如果极端情况下,CAS操作预期值总是不符合预期,那么cpu会不断循环重试,开销也存在一定问题。

3、只能保证一个共享变量的原子操作

如果需要同时修改多个变量,一个CAS恐怕不够。
java中提供了AtomicReference等引用类型可以使用。

三、原子类的使用

1、AtomicInteger

要注意辨析
public final int getAndIncrement() //得到原值后自增
public final int incrementAndGet() //自增后得到值

public class UseAtomicInt {
	
	static AtomicInteger ai = new AtomicInteger(10);
	
    public static void main(String[] args) {
    	System.out.println(ai.getAndIncrement());//10--->11
    	System.out.println(ai.incrementAndGet());//11--->12--->out
    	System.out.println(ai.get());
    }
}

2、AtomicReference

引用类型的原子操作类 要特别注意一个问题☝️,即并不会改变原实体类的引用。
下面代码的11、12行打印新对象信息,13、14行打印原对象的信息

public class UseAtomicReference {
	
	static AtomicReference<UserInfo> userRef = new AtomicReference<UserInfo>();
	
    public static void main(String[] args) {
        UserInfo user = new UserInfo("Mark", 15);//要修改的实体的实例
        userRef.set(user);
        
        UserInfo updateUser = new UserInfo("Bill", 17);//要变化的新实例
        userRef.compareAndSet(user, updateUser);
        System.out.println(userRef.get().getName());
        System.out.println(userRef.get().getAge());
        System.out.println(user.getName());
        System.out.println(user.getAge());        
    }
    
    //定义一个实体类
    static class UserInfo {
        private String name;
        private int age;
        public UserInfo(String name, int age) {
            this.name = name;
            this.age = age;
        }
        public String getName() {
            return name;
        }
        public int getAge() {
            return age;
        }
    }
}

3、AtomicStampedReference

这个类上面提到过,是用来解决ABA问题的,即在构造方法
public AtomicStampedReference(V initialRef, int initialStamp) ;
可以通过initialStamp参数定义一个版本号。

public class useAtomicStampedReference {
    static AtomicStampedReference<String> asr =
            new AtomicStampedReference<>("Mark", 0);


    public static void main(String[] args) throws InterruptedException {
        final int oldStamp = asr.getStamp();//那初始的版本号
        final String oldReferenc = asr.getReference();

        System.out.println(oldReferenc + "===========" + oldStamp);

        Thread rightStampThread = new Thread(() -> System.out.println(Thread.currentThread().getName()
                + "当前变量值:" + oldReferenc + "当前版本戳:" + oldStamp + "-"
                + asr.compareAndSet(oldReferenc, oldReferenc + "Java", oldStamp, oldStamp + 1)));

        Thread errorStampThread = new Thread(() -> {
            String reference = asr.getReference();
            System.out.println(Thread.currentThread().getName()
                    + "当前变量值:" + reference + "当前版本戳:" + asr.getStamp() + "-"
                    + asr.compareAndSet(reference, reference + "C", oldStamp, oldStamp + 1));

        });

        rightStampThread.start();
        rightStampThread.join();
        errorStampThread.start();
        errorStampThread.join();
        System.out.println(asr.getReference() + "===========" + asr.getStamp());

    }
}

上述代码中rightStampThread先执行
asr.compareAndSet(oldReferenc, oldReferenc + “Java”, oldStamp, oldStamp + 1)
四个参数的含义依次是,旧值,新值,旧的版本戳,新的版本戳

rightStampThread后执行
asr.compareAndSet(reference, reference + “C”, oldStamp, oldStamp + 1))
它还是使用定义在main方法中的旧版本戳oldStamp,由于这个版本戳已经被rightStampThread改变,所以造成rightStampThread中compareAndSet返回false
在这里插入图片描述
本章主要介绍了JAVA中原子操作CAS。读者要牢记CAS是由硬件指令来实现的,跟锁🔒不一样,相信就能辨析它在JAVA并发编程中的作用。

下一章:JAVA并发编程-4-显式锁Lock

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章