六、原子操作CAS

一、什么是原子操作?如何实现原子操作?

CAS:Compare And Swap,比较并且交换。隶属于乐观锁机制。
什么是原子操作?
假设现在有A,B两个操作,如果某个线程执行A操作,当另外一个线程执行B操作的时候,要么这个B全部执行完,要么这个B完全不执行,那么对于A、B来讲,他们彼此就是原子的。
在数据库层面,这种操作就是事务操作,严格意义上来说事务操作也是属于原子操作的一种
如何实现原子操作
可以利用synchronize关键字,但是会引发一系列问题:

  • 1.synchronize是阻塞式的,一个线程拥有锁后,其他的线程必须等待
  • 2.等待中的线程优先级很高,但是迟迟拿不到锁怎么办?
  • 3.等待中的线程竞争很激烈,但是拿到锁的线程迟迟不释放锁怎么办?
解决办法CAS

CAS可以完美地解决上述的问题,进而更完美地实现原子操作,它利用了现代处理器都支持的CAS指令,这个指令是CPU级别的指令。

CAS包含的要素

1.内存地址v:修改的对象或者变量的内存地址
2.期望值A:
3.新值B
当我去改这个内存地址上所对应的对象或者变量的时候,我期望在我改的时候,这个值是多少,如果是A,我就把他改成B,如果不是A,那我就不能改。将B值替换为A值。
比较---->交换

用java语言来讲,这个操作需要两个语句,一个是比较,一个是交换。
而在CPU层面,只要你执行了这个指令,我可以保证别的指令都被阻塞,只有这一个CAS指令操作完了才允许别的指令进行操作。

在JDK层面来讲,用到了循环(自旋、死循环),直到成功为止,原理如下:

15200008-8d4d8817e8da8bc9.png
原理

这种思想就是 乐观锁

用一句话来概括CAS如何实现线程安全?

CAS在语言层面不作处理,我们把它交给了CPU和内存,利用CPU的能力实现硬件层面阻塞,进而实现CAS的线程安全。

二、CAS引起的问题

1.ABA问题

下面的两种情况下会出现ABA问题。
1.A最开始的内存地址是X,然后失效了,又分配了B,恰好内存地址是X,这时候通过CAS操作,却设置成功了
  这种情况在带有GC的语言中,这种情况是不可能发生的,为什么呢?拿JAVA举例,在执行CAS操作时,A,B对象肯定生命周期内,GC不可能将其释放,那么A指向的内存是不会被释放的,B也就不可能分配到与A相同的内存地址,CAS失败。若在无GC的,A对象已经被释放了,那么B被分配了A的内存,CAS成功。
2.线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了,尽管CAS成功,但可能存在潜藏的问题。比如:

现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B:head.compareAndSet(A,B);在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A。而对象B此时处于游离状态:此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,其中堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。

以上就是由于ABA问题带来的隐患,各种乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题,在Java中,AtomicStampedReference<E>也实现了这个作用,它通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题,例如下面的代码分别用AtomicInteger和AtomicStampedReference来对初始值为100的原子整型变量进行更新,AtomicInteger会成功执行CAS操作,而加上版本戳的AtomicStampedReference对于ABA问题会执行CAS失败。

package concur.lock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABA {
    
    private static AtomicInteger atomicInt = new AtomicInteger(100);
    private static AtomicStampedReference<Integer> atomicStampedRef = 
            new AtomicStampedReference<Integer>(100, 0);
    
    public static void main(String[] args) throws InterruptedException {
        Thread intT1 = new Thread(new Runnable() {
            @Override
            public void run() {
                atomicInt.compareAndSet(100, 101);
                atomicInt.compareAndSet(101, 100);
            }
        });
        
        Thread intT2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                boolean c3 = atomicInt.compareAndSet(100, 101);
                System.out.println(c3);        //true
            }
        });
        
        intT1.start();
        intT2.start();
        intT1.join();
        intT2.join();
        
        Thread refT1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                atomicStampedRef.compareAndSet(100, 101, 
                        atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
                atomicStampedRef.compareAndSet(101, 100, 
                        atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
            }
        });
        
        Thread refT2 = new Thread(new Runnable() {
            @Override
            public void run() {
                int stamp = atomicStampedRef.getStamp();
                System.out.println("before sleep : stamp = " + stamp);    // stamp = 0
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());//stamp = 1
                boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp+1);
                System.out.println(c3);        //false
            }
        });
        
        refT1.start();
        refT2.start();
    }

}

如何解决?
增加版本号,也就是说在每个变量前面都要加一个版本号,每次修改的时候都对其版本+1。其实在大多数开发过程中,我们是不关心ABA问题的。但是ABA问题在一线互联网公司的面试中是经常问到的。

  • 1.ABA问题的解决思路是使用版本号,每次变量更新的时候版本号加1,那么A->B->A就会变成1A->2B->3A
  • 2.从jdk1.5开始,jdk的Atomic包里就提供了两个类来解决ABA问题,一个是AtomicStampedReference,另一个是AtomicMarkableReference,AtomicStampedReference这个类中的compareAndSet方法的作用就是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值更新为指定的新值。
    AtomicStampedReferenceAtomicMarkableReference的区别
    AtomicStampedReference带了版本号,关心被修改过几次,AtomicMarkableReference只关心有没有人修改过。
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。从java1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

三、原子操作类的使用

jdk中相关原子操作类的使用
  • 更新基本类型类:AtomicBoolean,AtomicInteger,AtomicLong
  • 更新数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
  • 更新引用类:AtomicReference,AtomicMarkableReference,AtomicStampeReference
  • 原子更新字段类:AtomicReferenceFiledUpdater,AtomicIntegerFiledUpdater,AtomicLongFiledUpdater
举例:
import java.util.concurrent.atomic.AtomicInteger;

/**
 *类说明:演示基本类型的原子操作类
 */
public class UseAtomicInt {
    static AtomicInteger ai = new AtomicInteger(10);

    public static void main(String[] args) {
        //返回的是我自增以前的值
        int i =  ai.getAndIncrement(); // i++
        //返回自增以后的值
        int b = ai.incrementAndGet();// ++i
        System.out.println(i +"------"+ b);
        //ai.compareAndSet();
        int fianl = ai.addAndGet(24);
        System.out.println("加了24之后的值为:"+fianl);
    }
}

运行结果:


15200008-c64b26a2b3a1e306.png
原子操作类的使用
import java.util.concurrent.atomic.AtomicIntegerArray;


/**
 *类说明: 演示原子操作数组
 */
public class AtomicArray {
    static int[] value = new int[] { 1, 2 };
    static AtomicIntegerArray ai = new AtomicIntegerArray(value);
    public static void main(String[] args) {
        ai.getAndSet(0, 3);
        System.out.println(ai.get(0));
        System.out.println(value[0]);//原数组不会变化
        }
}

运行结果:


15200008-4050de58d8125ad7.png
原子操作数组
注意:

原子操作只会操作原子类的值,不会操作原数组,原子操作类的值再怎么变也不会影响原数组的值

运用原子操作类修改两个变量的值
import java.util.concurrent.atomic.AtomicReference;

/**
 *类说明:演示引用类型的原子操作类
 */
public class UseAtomicReference {
    static AtomicReference<UserInfo> atomicUserRef;
    public static void main(String[] args) {
        UserInfo user = new UserInfo("Mark", 15);//要修改的实体的实例
        atomicUserRef = new AtomicReference(user);
        UserInfo updateUser = new UserInfo("Bill",17);
        atomicUserRef.compareAndSet(user,updateUser);

        System.out.println(atomicUserRef.get());
        System.out.println(user);
    }
    
    //定义一个实体类
    static class UserInfo {
        private volatile 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;
        }

        @Override
        public String toString() {
            return "UserInfo{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }

}

运行结果:


15200008-0e688c87039ebfc9.png
AtomicReference

这是运用AtomicReference修改两个变量的值,本质上是包装成一个变量,对这一个变量进行修改。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值