在计算机科学中,比较和交换(Conmpare And Swap)是用于实现多线程同步的原子指令。 它将内存位置的内容与期望值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。 这是作为单个原子操作完成的。 原子性保证新值基于最新信息计算; 如果该值在同一时间被另一个线程更新,则写入将失败。 操作结果必须说明是否进行替换; 这可以通过一个简单的布尔响应(这个变体通常称为比较和设置),或通过返回从内存位置读取的值来完成。
JAVA1.5开始引入了CAS,主要代码都放在JUC的atomic包下。
通过源码:
如果这个类的期望值(此刻的值)是0,那么就将更新的值赋值给这个类,成为该类的最新值,成功赋值返回true,否则返回false。
Unsafe类是java底层源码,native修饰的,通过C++语言,通过原语对cpu进行操作。原语是若干个指令组成,并且必须是连续执行来完成一个任务,过程中不能被其他线程插入。
通过AtomicInteger atomicInteger = new AtomicInteger(0);
的atomicInteger.getAndIncrement();
方法,得知源码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JZOXqZUH-1592126833111)(.\pictureForJUC\CAS底层.png)]
在执行CAS语句时:var1,var2获得内存此刻的值,与之前获得的内存值(期望值)var5
,进行比较,比较相同,则进行var5=var5+var4
并返回var5
如线程1从内存X中取出A,这时候另一个线程2也从内存X中取出A,并且线程2进行了一些操作将内存X中的值变成了B,然后线程2又将内存X中的数据变成A,这时候线程1进行CAS操作发现内存X中仍然是A,然后线程1操作成功。虽然线程1的CAS操作成功,但是整个过程就是有问题的。
所以JAVA中提供了AtomicStampedReference
或AtomicMarkableReference
原子引用来处理会发生ABA问题的场景,主要是在对象中额外再增加一个标记来标识对象是否有过变更。
AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(1,1);
// 第一个参数值是Reference值,就是需要更新的Interge类型的值
// 第二个参数值是stamp值,用于记录版本信息,用于解决ABA的问题
大坑就在此,AtomicStampedReference的构造器:
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
这里的Pair是AtomicStampedReference的内部类,用于存储更新值和邮票值,存储的Integer类型的数字在java中是有缓存的,缓存范围是-128~127
,所以当AtomicStampedReference类初始化为这个之间的数字时,自动从缓存中获取,因此地址也是引用的缓存中的地址。在后续进行比较交换时:
stampedReference.compareAndSet(
1,
2,
stampedReference.getStamp(),
stampedReference.getStamp() + 1);
期望值是1,1在缓存中,会引用缓存中的内存地址,并且初始时也是1,期望值于当前值的引用地址都是一样的,compareAndSet方法通过==进行判断值期望值于当前值是否相等:
因此不会有任何错误,比较交换都会成功返回ture。
但是:如果你初始化为1130并且更新值时,也是输入的1130为期望值:
AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(1130,1);
// 更新值方法
stampedReference.compareAndSet(
1130,
2,
stampedReference.getStamp(),
stampedReference.getStamp() + 1);
这种结果会出错,会返回false。这时由于当初始化时,传入的1130已经超过了缓存值,会创建一个新的内存空间存储这个Integer类型的值,而当调用compareAndSet
时,也传入了1130,这个也已经超过了缓存值,因此又会创建一个内存空间存储这个Integer值,由于compareAndSet
方法是通过==
进行比较的,不是通过equals
进行比较值的,因此期望值于当前值的引用地址不一样导致无法更新值成功,所以会返回false。
解决办法:
既然知道原因了,那么就很好解决了,在更新值的时候,直接使用stampedReference.getReference()
方法获得当前值作为期望值就可以了。这样期望值和当前值的引用地址都一样,可以确保交换成功!,此时就返回ture了。
stampedReference.compareAndSet(
stampedReference.getReference(),
2,
stampedReference.getStamp(),
stampedReference.getStamp() + 1)
实例:
public class Test01{
public static void main(String[] args) throws Exception {
AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(1,1);
int stamp = stampedReference.getStamp(); // 获取当前邮票值 为 1
new Thread(()->{
// 对值进行CAS操作,并将邮票更新
System.out.println("A "+stampedReference.compareAndSet(
1,
2,
stampedReference.getStamp(),
stampedReference.getStamp() + 1));
System.out.println("a1 => "+stampedReference.getStamp());
// 对值进行CAS操作,并将邮票更新
System.out.println("A "+stampedReference.compareAndSet(
2,
1,
stampedReference.getStamp(),
stampedReference.getStamp() + 1));
System.out.println("a2 => "+stampedReference.getStamp());
}).start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 对值进行CAS操作,期望值与内存值相同,会更新为3。但是内存邮票值与期望邮票值不同,所以整个CAS操作失败。
// 因此可以解决ABA的问题,就不会出现原有的ABA的问题了。
System.out.println("B "+stampedReference.compareAndSet(
1,
3,
stamp,
stampedReference.getStamp() + 1));
System.out.println("b1 => "+stampedReference.getStamp());
}).start();
}
}
输出:
A true
a1 => 2
A true
a2 => 3
B false
b1 => 3
该原子引用,虽然使用了ABA的方式,线程A将原来的值赋了新值之后又将其赋值到原来的值,但是线程B操作的时候,会发现邮票值有变动,因此会操作失败。解决了ABA的问题。
(悲观锁) 使用synchronized关键字,只允许一个线程进入同步代码块进行操作,而其他线程就必须在同步代码块外等待,此时其他线程就进入了BLOCKED状态,不占用CPU资源。而当前线程操作完之后,会释放锁,其他线程会去抢锁,如果抢到了就会被掉到CPU中进行执行。很明显,使用synchronized的同步代码,线程会在阻塞状态进入运行状态之间切换,有一个上下文切换的过程,该过程会浪费时间。
上下文切换:
同步代码中,一个线程拿到锁从阻塞状态进入运行状态的时候,就被调度CPU中执行,一个线程除了去执行代码之外,线程本身也是有一些数据的,这些数据会调度到CPU cache中;而执行完之后,会从运行状态进入阻塞状态或是其他状态,这时就会将该线程的数据和线程从CPU中拿出去,换下一个进程进来。这样个过程就是上下文切换。
(乐观锁) 使用CAS操作,CAS是比较并交换,因为CAS操作时原子性的,始终只会存在一个线程去执行CAS,所以不会存在线程安全问题。那么就会使得所有线程去获操作的时候,都会去比较,如果比较正确就执行交换,如果不正确就再次循环去拿新的值。当一个线程在CAS操作时,其他线程都会一直循环,空轮训,所有线程一直都是在执行中,少了上下文的切换。所以CAS操作效率会高很多,执行速度会快很多。
总结: 由于synchronized在线程调度上有上下文切换,浪费很多时间,而CAS操作的每一个线程都是一直循环然后比较值,没有上下文的切换,所以CAS会快很多。