CAS和自旋锁

什么是CAS

CAS算法Compare And Swap),即比较并替换,是一种实现并发编程时常用到的算法,Java并发包中的很多类都使用了CAS算法。

CAS算法有3个基本操作数:

  • 内存地址V
  • 旧的预期值A
  • 要修改的新值B

CAS使用自旋的方式来交换值,操作步骤为:

  1. 读取内存地址V的值保存在A中
  2. 在原子操作中比较内存地址V的值是否与A相同
  3. 相同时,修改内存地址V的值为B,原子操作成功。
  4. 不相同时,循环执行第一至第三步(自旋),直到成功。

什么是自旋锁?

自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。

对于互斥锁,会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。但是自旋锁不会引起调用者堵塞,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁。

自旋锁的实现基础是CAS算法机制。CAS自旋锁属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。

基于CAS实现-原子操作类

先看一个线程不安全的例子:

public class AutomicDemo {
    public static int num = 0;

    public static void main(String[] args){
        for(int i=0; i<5; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    for(int j=0; j<200; j++){
                        num++;
                    }
                }
            }).start();
        }

        /* 主控失眠3秒,保证所有线程执行完成 */
        try {
            Thread.sleep(15000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("num=" + num);
    }
}

输出结果:

num=950

因为自增操作不是原子性,多线程环境下,访问共享变量线程不安全。

解决方法,加synchronized同步锁:

for(int j=0; j<200; j++){
    synchronized (AutomicDemo.class){
        num++;
    }
}

输出结果:
num=1000

线程安全。

synchronized确保了线程安全,但会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。

尽管Java1.6为Synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。

于是JDK提供了一系列原子操作类:AtomicIntegerAtomicLongAtomicBooleanAtomicReference等,它们都是基于CAS去实现的,下面我们就来详细看一看原子操作类。

public class AutomicDemo {
    public static AtomicInteger num = new AtomicInteger(0);

    public static void main(String[] args){
        for(int i=0; i<5; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    for(int j=0; j<200; j++){
                        num.incrementAndGet();
                    }
                }
            }).start();
        }

        /* 主控失眠3秒,保证所有线程执行完成 */
        try {
            Thread.sleep(15000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("num=" + num.get());
    }
}

输出:
num=1000

使用AtomicInteger之后,最终的输出结果同样可以保证是200。并且在某些情况下,代码的性能会比Synchronized更好。

原子类操作方法

  • public final int get():取得当前值
  • public final void set(int newValue):设置当前值
  • public final int getAndSet(int newValue):设置新值并返回旧值
  • public final boolean compareAndSet(int expect, int update):如果当前值为expect,则设置为update
  • public final boolean weakCompareAndSet(int expect, int update):如果当前值为expect,则设置为update,可能失败,不提供保障
  • public final int getAndIncrement():当前值加1,返回旧值
  • public final int getAndDecrement():当前值减1,返回旧值
  • public final int getAndAdd(int delta):当前值加delta,返回旧值
  • public final int incrementAndGet():当前值加1,返回新值
  • public final int decrementAndGet():当前值减1,返回新值
  • public final int addAndGet(int delta):当前值加delta,返回新值

AtomicInteger底层原理

所有Atomic相关类的实现都是通过CAS(Compare And Swap)去实现的,它是一种乐观锁的实现。

CAS实现放在 Unsafe 这个类中的,其内部大部分是native方法。这个类是不允许更改的,而且也不建议开发者调用,它只是用于JDK内部调用,看名字就知道它是不安全的,因为它是直接操作内存,稍不注意就可能把内存写崩。

Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力。有一下特点:
1、不受jvm管理,也就意味着无法被GC,需要我们手动GC,稍有不慎就会出现内存泄漏。
2、Unsafe的不少方法中必须提供原始地址(内存地址)和被替换对象的地址,偏移量要自己计算,一旦出现问题就是JVM崩溃级别的异常,会导致整个JVM实例崩溃,表现为应用程序直接crash掉。
3、直接操作内存,也意味着其速度更快,在高并发的条件之下能够很好地提高效率。

去看AtomicInteger的内部实现可以发现,全是调用的Unsafe类中的方法:

CAS和自旋锁_第1张图片

原子操作类AtomicInteger详解

该文章详细介绍所有原子类,后续有时间整理学习

https://blog.csdn.net/fanrenxiang/article/details/80623884

CAS机制ABA问题

ABA问题:CAS算法通过比较变量值是否相同来修改变量值以保证原子性,但如果一个内存地址的值本身是A,线程1准备修改为C。在这期间,线程2将值修改为B,线程3将值修改为A,线程1获取内存地址的值还是A,故修改为C成功。但获取的A已不再是最开始那一个A。这就是经典的ABA问题,A已不再是A。

如果解决ABA问题呢?

两个方法,1、增加版本号;2、增加时间戳。

  • 增加版本号:让值的修改从A-B-A-C变为1A-2B-3A-4C;这样在线程1 中就能判别出1A不是当前内存中的3A,从而不会更新变量为4C。
  • 增加时间戳:值被修改时,除了更新数据本身外,还必须更新时间戳。对象值以及时间戳都必须满足期望,写入才会成功。JDK提供了一个带有时间戳的CAS操作类AtomicStampedeReference。

CAS的缺点

Java原子类使用自旋的方式来处理每次比较不相同时后的重试操作,下面来看看AtomicInteger类incrementAndGet方法的代码:

//AtomicInteger 的incrementAndGet方法,变量U为静态常量jdk.internal.misc.Unsafe类型
public final int incrementAndGet() {
  //使用getAndAddInt方法,实际操作类似j++
  return U.getAndAddInt(this, VALUE, 1) + 1;
}
//jdk.internal.misc.Unsafe类型的getAndAddInt方法
public final int getAndAddInt(Object o, long offset, int delta) {
  int v;
  do {
    //获取变量o的可见值
    v = getIntVolatile(o, offset);
    //比较与替换变量o(CAS算法)
  } while (!weakCompareAndSetInt(o, offset, v, v + delta));
  return v;
}
//jdk.internal.misc.Unsafe类型的weakCompareAndSetInt方法
public final boolean weakCompareAndSetInt(Object o, long offset,
                                              int expected,
                                              int x) {
  //执行比较与替换
  return compareAndSetInt(o, offset, expected, x);
}

在Unsafe类的getAndAddInt方法中使用了do…while循环语句,循环语句的作用为自旋,Unsafe的weakCompareAndSetInt实现CAS算法。

如果weakCompareAndSetInt一直不成功将会一直自旋下去,这将消耗过多的CPU时间。而且原子类使用CAS算法实现,这导致原子类只能保证一个变量的原子操作,对于需要保证一个具有多个操作的事务将变得无能为力。

总结如下:

  1. CPU开销较大
    在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,自旋会给CPU带来很大的压力
  2. 不能保证代码块的原子性
    CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

为应对CAS存在缺点,替换方案如下:

  1. 自旋效率:Java提供了自适应自旋锁
  2. 片面性:应对片面性问题Java提供了读写锁
  3. ABA问题:用AtomicStampedReference/AtomicMarkableReference解决ABA问题

参考文章:
漫画:什么是 CAS 机制?
知识点: JAVA 悲观锁与乐观锁原理分析 ABA与自旋效率问题分析及解决

你可能感兴趣的:(Java并发编程,Java,InterView,java)