一、概述
1.1 为什么多线程情况下,原子性操作会变得不安全
原子性操作可以是一个步骤,也可以是多个步骤,但其顺序不能被打乱,执行过剩中也不能被打断。原子性的核心特征就是将整个操作视为一个整体。上面的例子中,虽然this.count ++是一个语句,但其实是以下3个步骤的顺序执行:
- 加载count;
- count+1;
- 赋值count;
在单线程下执行,不会有任何线程安全问题,但是如果在多线程并发执行的情况下,会有线程安全问题。先用3个线程同时将count自增100,我们是想要的结果是300,但大多数情况下并不能达到理想的300。
public class Demo21 {
private volatile static int count;
public static void main(String[] args) throws Exception {
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i1 = 0; i1 < 100; i1++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
}
}).start();
}
Thread.sleep(5000);
System.err.println("count = "+count);
}
}
执行结果:
268
造成这样的原因,就是在多线程情况下,count++这个操作不原子:比如说thread01、thread02同时读取到count=0,然后再同时执行count+1,这时,两者持有的count都是1,然后再同时回写到主内存中,最终count的值还是1,而不是执行了两次+1操作的2,即:此时两者的操作不原子了。
理想的原子性操作,应该是thread01执行完count++操作,主内存中的count值已是1的情况下,thread02或者其他线程再去执行count++操作:

一种很容易想到的方式就是直接加个synchronized锁,这样保证了数据的准确性,但是,这不就是变成多线程的串行执行了,本质上变成了单线程操作,却比单线程多了加锁解锁操作,似乎有点本末倒置。
二、JUC包下提供的原子操作类
java.util.concurrent包下为我们提供了一些基本数据类型对应的原子操作类,这些类在执行类似于i++操作时

我们将改用AtomicInteger,再试一下,可以看到最终的结果始终是300:
public class Demo21 {
private volatile static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws Exception {
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i1 = 0; i1 < 100; i1++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
count.getAndIncrement();
}
}
}).start();
}
Thread.sleep(5000);
System.err.println("count = "+count);
}
}
现在问题暂时是解决了,但是JDK是怎么做到的呢?
三、CAS机制(自旋锁)
CAS(compare and swap):比较和交换,是硬件级的一个操作,从CPU底层保证了该操作的原子性(是一个native方法)。
CAS操作需要输入两个数值:旧值、新值。CAS在操作期间会去比较旧值是否发生变化,如果未发生变化,则将新值写入,如果操作期间旧值发生了变化,则不操作。
JDK中JUC包下的原子操作类内部就是使用了CAS操作,现在截取AtomicInteger的getAndIncrement方法看一下:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
// 下面是Unsafe对象的getAndAddInt方法:
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;
}
现在我们自己来手动调用一下Unsafe对象的getAndAddInt方法,由于Unsafe对象是不能直接获取的坑,所以我们通过反射类获取,最终结果同样也是300:
public class Demo22 {
// count变量的偏移量(用于内存寻址)
private static final long offset;
private static volatile int count;
private static Unsafe unsafe;
static {
try {
unsafe = (Unsafe)Class.forName("Unsafe").newInstance();
// 获取count变量的偏移量
offset = unsafe.objectFieldOffset(Demo22.class.getDeclaredField("count"));
} catch (Exception ex) { throw new Error(ex); }
}
public static void main(String[] args) throws Exception {
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i1 = 0; i1 < 100; i1++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 自增
do {
count = unsafe.getIntVolatile(count, 1);
} while(!unsafe.compareAndSwapInt(count, offset, 1, count + 1));
}
}
}).start();
}
Thread.sleep(5000);
System.err.println("count = "+count);
}
}
3.2 CAS机制的局限性
3.2.1 额外资源消耗
可以看到,CAS是通过while循环实现的,如果一直操作不成功,就不断的获取旧值,再执行CAS操作。
而如果长时间不成功,会带来很大的CPU资源消耗。
3.2.2 仅能操作单个变量
CAS机制只能操作单个变量,而不能用于多个变量的原子性操作
3.2.3 ABA问题
ABA问题是指在CAS操作时,其他线程将变量值A改为了B,但是又被改回了A,等到本线程使用期望值A与当前变量进行比较时,发现变量A没有变,于是CAS就将A值进行了交换操作,但是实际上该值已经被其他线程改变过,这与乐观锁的设计思想不符合。
ABA问题的解决思路是,每次变量更新的时候把变量的版本号加1,那么A-B-A就会变成A1-B2-A3,只要变量被某一线程修改过,改变量对应的版本号就会发生递增变化,从而解决了ABA问题。在JDK的JUC包中提供了AtomicStampedReference(可以知道改变了几次)、AtomicStampedReference来解决ABA问题,两者的构造器内部,都实例化了一个Pair对象,Pair对象记录了对象引用和时间戳信息,compareAndSet是核心方法,实现如下(每次操作都会去检查当前引用与当前标志是否与预期相同):
// 以AtomicMarkableReference为例
public boolean compareAndSet(V expectedReference,
V newReference,
boolean expectedMark,
boolean newMark) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedMark == current.mark &&
((newReference == current.reference &&
newMark == current.mark) ||
casPair(current, Pair.of(newReference, newMark)));
}
AtomicStampedReference利用一个int类型的标记来记录,它能够记录改变的次数。
而AtomicMarkableReference利用一个boolean类型的标记来记录,只能记录它改变过,不能记录改变的次数
参考:
AtomicReference,AtomicStampedReference与AtomicMarkableReference的区别