JAVA并发编程之——CAS

时间:2021-11-21 18:01:57

锁的劣势

锁机制存在以下问题:
- 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
- 一个线程持有锁会导致其它所有需要此锁的线程挂起。在挂起和恢复线程等过程中存在着很大的开销,并且通常存在着较长时间的中断。
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

volatile变量是一种更轻量级的同步机制(使用这些变量时不会发生上下文的切换或线程调度),但是volatile不能保证原子性。

CAS概述

CAS,Compare And Swap,即比较并交换。CAS是JDK中concurrent并发包的基础,整个AQS同步组件、Atomic原子类操作等等都是以CAS实现的,甚至ConcurrentHashMap在1.8的版本中也调整为了CAS+Synchronized。

CAS包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。当且仅当V的值等于A时,CAS才通过原子方式用新值B来更新V的值A,否则不会执行任何操作。无论位置V的值是否等于A,都将返回V原有的值。

当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都将失败。然而,失败的线程并不会被挂起(当获取锁失败,线程被挂起),而是被告知这次竞争失败,并可以再次尝试。

CAS的问题

ABA问题:因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,仍认为是发生了变化。

CAS的ABA问题的解决方案则是版本号,Java提供了AtomicStampedReference来解决。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

AtomicStampedReference源码

Pair内部类

Pair类来记录对象引用和时间戳信息,采用int作为时间戳,实际使用的时候时间戳信息要做成自增的,否则时间戳如果重复,还会出现ABA的问题。这个Pair对象是不可变对象,所有的属性都是final的,of方法每次返回一个新的不可变对象。

private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}

private volatile Pair<V> pair;

Set操作

set方法时,当要设置的对象和当前Pair对象不一样时,新建一个不可变的Pair对象



public void set(V newReference, int newStamp) {
Pair<V> current = pair;
if (newReference != current.reference || newStamp != current.stamp)
this.pair = Pair.of(newReference, newStamp);
}

compareAndSet有四个参数,分别表示:预期引用、更新后的引用、预期标志、更新后的标志。如果更新后的引用和标志和当前的引用和标志相等则直接返回true,否则通过Pair生成一个新的pair对象与当前pair CAS替换


public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}

AtomicStampedReference Demo

public class CAS_ABA {

private static AtomicInteger atomicInt = new AtomicInteger(100);
private static AtomicStampedReference atomicStampedRef = new AtomicStampedReference(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) {
}
boolean c1 =atomicInt.compareAndSet(100,101);
System.out.println("AtomicInteger=====CAS 更新操作===" +c1+"===ABA问题出现");
}
});

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();
}
System.out.println("AtomicStampedReference===版本号正常获取=====");
boolean c1 = atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
boolean c2 = atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
System.out.println("AtomicStampedReference===版本号正常获取=====结果=="+c1+"=="+c2);
}
});

Thread refT2 = new Thread(new Runnable() {
@Override
public void run() {
int stamp = atomicStampedRef.getStamp();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("AtomicStampedReference===版本号迟滞获取=====");
boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
System.out.println("AtomicStampedReference===版本号迟滞获取=====结果==="+c3);
}
});

refT1.start();
refT2.start();
}
}

打印结果

AtomicInteger=====CAS 更新操作===true===ABA问题出现
AtomicStampedReference===版本号正常获取=====
AtomicStampedReference===版本号正常获取=====结果==true==true
AtomicStampedReference===版本号迟滞获取=====
AtomicStampedReference===版本号迟滞获取=====结果===false