欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java并发编程:什么是CAS?这回总算知道了

程序员文章站 2022-03-16 22:45:42
无锁的思想 众所周知,Java中对并发控制的最常见方法就是锁,锁能保证同一时刻只能有一个线程访问临界区的资源,从而实现线程安全。然而,锁虽然有效,但采用的是一种悲观的策略。它假设每一次对临界区资源的访问都会发生冲突,当有一个线程访问资源,其他线程就必须等待,所以锁是会阻塞线程执行的。 当然,凡事都有 ......

无锁的思想

众所周知,java中对并发控制的最常见方法就是锁,锁能保证同一时刻只能有一个线程访问临界区的资源,从而实现线程安全。然而,锁虽然有效,但采用的是一种悲观的策略。它假设每一次对临界区资源的访问都会发生冲突,当有一个线程访问资源,其他线程就必须等待,所以锁是会阻塞线程执行的。

当然,凡事都有两面,有悲观就会有乐观。而无锁就是一种乐观的策略,它假设线程对资源的访问是没有冲突的,同时所有的线程执行都不需要等待,可以持续执行。如果遇到冲突的话,就使用一种叫做cas (比较交换) 的技术来鉴别线程冲突,如果检测到冲突发生,就重试当前操作到没有冲突为止。

cas概述

cas的全称是 compare-and-swap,也就是比较并交换,是并发编程中一种常用的算法。它包含了三个参数:v,a,b。

其中,v表示要读写的内存位置,a表示旧的预期值,b表示新值

cas指令执行时,当且仅当v的值等于预期值a时,才会将v的值设为b,如果v和a不同,说明可能是其他线程做了更新,那么当前线程就什么都不做,最后,cas返回的是v的真实值。

而在多线程的情况下,当多个线程同时使用cas操作一个变量时,只有一个会成功并更新值,其余线程均会失败,但失败的线程不会被挂起,而是不断的再次循环重试。正是基于这样的原理,cas即时没有使用锁,也能发现其他线程对当前线程的干扰,从而进行及时的处理。

cas的应用类

java中提供了一系列应用cas操作的类,这些类位于java.util.concurrent.atomic包下,其中最常用的就是atomicinteger,该类可以看做是实现了cas操作的integer,所以,下面我们就通过学习该类的案例来一窥全貌cas的妙用。

学习atomicinteger之前,我们先来看一段代码实例:

public class atomicdemo {

    public static int number = 0;

    public static void increase() {
        number++;
    }

    public static void main(string[] args) throws interruptedexception {
        atomicdemo test = new atomicdemo();
        for (int i = 0; i < 10; i++) {
            new thread(() -> {
                for (int j = 0; j < 1000; j++)
                    test.increase();
            }).start();
        }
        thread.sleep(200);
        system.out.println(test.number);
    }
}

在main函数中开启了10个线程,执行后会轮流调用 increase(),当然我们知道,运行后输出的结果肯定不是我们期望的值,因为没有做线程安全的处理,所以10个线程流量操作临界区的资源number就会出错。

解决办法并不难,用我们之前学过的锁,例如synchronized修饰代码块,程序就会正常输出10000。当然,用锁解决并不是我们想要的方式,因为锁会阻塞线程,影响程序的性能,这时候,atomicinteger就可以派上用场了。

将上面的程序改造一下,变成下面这样:

public static atomicinteger number = new atomicinteger(0);

public static void increase() {
    number.getandincrement();
}

public static void main(string[] args) throws interruptedexception {
    atomicdemo test = new atomicdemo();
    for (int i = 0; i < 10; i++) {
        new thread(() -> {
            for (int j = 0; j < 1000; j++)
                test.increase();
        }).start();
    }
    thread.sleep(200);
    system.out.println(test.number);
}

运行main方法,程序输出的就是我们想要的值,也就是10000。

上面的代码中,increase方法里调用了number.getandincrement() ,这是atomicinteger的自增方法,会对当前的值加1,并且返回旧值,点进方法的源码,它调用的是unsafe.getandaddint()方法:

public final int incrementandget() {
    return unsafe.getandaddint(this, valueoffset, 1) + 1;
}

getandaddint的作用是对当前值加1,并返回旧值。

unsafe是unsafe类的一个变量,通过unsafe.getunsafe()来获取

private static final unsafe unsafe = unsafe.getunsafe();

unsafe类是一个比较特殊的类,它是一个jdk内部使用的专属类,用一般的编辑器无法直接查看源码,只能看到反编译后的class文件。

这里要扩展一个知识点,就是java本身无法访问操作系统,需要使用native方法,而unsafe类中的方法就包含了大量的native方法,提高了java对系统底层的原子操作能力。例如我们代码中使用到的getandaddint()底层就是调用一个native方法,用idea点击方法,得到下面反编译后的代码:

public final int getandaddint(object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getintvolatile(var1, var2);
    } while(!this.compareandswapint(var1, var2, var5, var5 + var4));

    return var5;
}
public final native boolean compareandswapint(object var1, long var2, int var4, int var5);

compareandswapint的作用是比较并交换整数值,如果指定的字段的值等于期望值,也就是cas中的 'a' (预期值),那么就会把它设置为新值 (cas中的 'b'),不难想象,该方法内部的实现必然是依靠原子操作完成的。除此之外,unsafe类中还提供了其他的原子操作的方法,例如上面源码中的getintvolatile就是使用volatile语义获得给定对象的值,这些方法通过底层的原子操作高效的提升了应用层面的性能。

cas的缺点

虽然cas的性能比起锁要强大很多,但它也存在一些缺点,例如:

1、循环的时间开销大

在getandaddint的方法中,我们可以看到,只是简单的设置一个值却调用了循环,如果cas失败,会一直进行尝试。如果cas长时间不成功,那么循环就会不停的跑,无疑会给系统造成很大的开销。

2、aba问题

前面说过,cas判断变量操作成功的条件是v的值和a是一致的,这个逻辑有个小小的缺陷,就是如果v的值一开始为a,在准备修改为新值前的期间曾经被改成了b,后来又被改回为a,经过两次的线程修改对象的值还是旧值,那么cas操作就会误任务该变量从来没被修改过。这就是cas中的“aba”问题。

当然,"aba"问题也有解决方案,java并发包中提供了一个带有时间戳的对象引用 atomicstampedreference,其内部不仅维护了一个对象值,还维护了一个时间戳,当atomicstampedreference对应的数值被修改时,除了更新数据本身,还需要更新时间戳,只有对象值和时间戳都满足期望值,才能修改成功。这是atomicstampedreference的几个有关时间戳信息的方法:

//比较设置 参数依次为:期望值 写入新值 期望时间戳 新时间戳
public boolean compareandset(v expectedreference, v newreference,
                             int expectedstamp, int newstamp)
//获得当前时间戳
public int getstamp()
//设置当前对象引用和时间戳
public void set(v newreference, int newstamp)