本文乃个人拙见,如有错误,欢迎指出,不能误导新人
上一节讲到了CAS,最后说到了CAS的缺点,如果有很多写操作的话,CAS的性能会降低,这个时候还是使用synchronized比较合适。但是CAS还存在一个问题就是ABA问题。
ABA问题
之前说到,compareAndSwap,是比较持有值与内存值是否相等,然后进行插入,乍一看没什么毛病,如果A、B两个线程同时取到了主内存中的i=1,这个时候B线程操作比较快,将i写成2,然后写入主内存中,然后这个时候B线程很快速的又修改了一次,将2修改为了1,这个时候A线程调用CAS,发现内存中是1没问题,进行修改,这个时候其实中间就有了猫腻,如果是在栈中修改值的话,t1和t2同时对A进行操作,这个时候t2将栈中元素从A修改为了ABA,而t1线程比较A的时候发现两个值是相等的于是进行操作,这个时候是出问题的。
解决方案
在JUC中的atomic包中有一个AtomicStampeReference类,这个类在每一次修改值的时候加一个版本号(stamp),就好比svn一样,每一次加一,这样要是t2线程进行修改两次的话,版本号就会变成3,而t1线程在去修改的时候虽然值相同,但是t1期望的版本为1,所以修改失败,
package aba;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABADemo2 {
static AtomicReference<Integer> atomicInteger = new AtomicReference<>(100);
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
atomicInteger.compareAndSet(100,101);
atomicInteger.compareAndSet(101,100);
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
},"t1").start();
new Thread(() -> {
try {
int stamp = atomicStampedReference.getStamp();
TimeUnit.SECONDS.sleep(3);
System.out.println("======atomicInteger cas操作");
System.out.println(atomicInteger.compareAndSet(100, 2019)+"\t"+atomicInteger.get());
System.out.println("======atomicStampedReference cas操作");
System.out.println(atomicStampedReference.compareAndSet(100,102,stamp,stamp+1));
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t2").start();
TimeUnit.SECONDS.sleep(2);
}
}
代码中定义了一个atomicInteger,和一个atomicStampedReference,可以看到的是使用了atomicStampedReference防止了ABA操作。
AtomicReference
JUC为我们准备了AtomicInteger,AtomicLong…但是这些个类不能同时保证多个变量的原子性,如果我们需要其他类型呢?这个时候就需要AtomicRefrence(原子引用)来解决,在上面的AtomicStampedReference其实就是一个原子引用,可以传入自己的对象,
package aba;
import java.util.concurrent.atomic.AtomicReference;
class User{
String userName;
int age;
public User(String userName, int age) {
this.userName = userName;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", age=" + age +
'}';
}
}
public class ABADemo {
public static void main(String[] args) {
User z3 = new User("z3",13);
User li4 = new User("li4",32);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(z3);
System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.toString());
System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.toString());
}
}
代码中将一个User类传入,然后直接传入期望类,以及改变的类就可以了。
在atomic包里面还有一个AtomicMarkableReference,其原理基本和AtomicStampedReference是一样的,AtomicMarkableReference不是用版本号控制的,而是用一个标记true,false来标记。
最后
AtomicIntegerArray,看这个类的set方法,实现原理基本相同,以及AtomicLongArray
public final void set(int i, int newValue) {
unsafe.putIntVolatile(array, checkedByteOffset(i), newValue);
}
最后说四个,DoubleAccumulator,DoubleAdder,LongAccumulator,LongAdder这四个类是干嘛用的?Accumulator计算,这四个是用在高并发下面的,就是很多写操作的时候,他们使用了cell,每个线程都先写在各自的cell中,然后最后做统计,也就是说,在高并发下面这四个类的速度要快于atomic的类,并且基本是用来做统计用的,并不会来单独跟新值,在低并发下面这四个类和atomic类都差不多,这中思想有点象MapReduce了。