原子操作的意思是"不可被中断的一个操作或者一系列操作"
实现方式
- 使用循环CAS实现原子操作
- 使用锁机制实现原子操作
首先我们看一个例子,10个线程同时存钱,每个线程每次存10000,最终我们想看的结果应该是10*10000 = 100000,但是结果呢?
public class Test {
private static final int THREDS_COUNT = 10;
public static void main (String[] args) {
for (int i = 0; i < THREDS_COUNT; i++) {
Thread thread = new Thread(new IncreaseThread());
thread.start();
}
}
}
class IncreaseThread implements Runnable {
public static volatile int money = 0;
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
money++;
}
System.out.println(money);
}
}
//输出结果
11995
26026
16026
41034
43127
53127
63590
72453
82453
92453
从运行结果可以看出好端端的怎么少了好几千块钱,这是为什么呢,为什么已经将money定义为volatile变量还不能保证原子性操作呢?首先我们来看一下volatile的定义
volatile
Java语言规范第三版中对volatile的定义如下:Java编程语言允许线程访问共享变量,为了确保共享变量能够被准确和一致性地更新,线程应该确保通过排它锁获得这个变量。
- volatile关键字可以保证此变量对所有线程都是可见的,即变量被修改的时候,其他线程会立刻知道变量值已经更新,下次操作的时候不会再从缓存中读取失效变量,而是从内存中读取更新的变量到缓存。
- 禁止指令重排。
那既然这样,在money参数发生变化的时候,其他线程已经知晓money已经发生变化,那为什么还是出现错误呢,这是因为例如i++这种操作时读改写操作,原因是,多个处理器从各自的缓存中读取了变量i,分别进行了+1操作,在CPU1进行操作的时候,CPU2也在进行操作,最后都会将更新的值写回到各自缓存,然后刷新回内存,例如CPU1和CPU2同时读取的是1并各自操作都进行了自加,即CPU1和CPU2处理完分别写回到缓存均为2,按照我们的初衷,应该结果是3,可以看出他们是同时进行了操作,所以要解决这种情况就必须保证在CPU1进行读改写操作的同时,CPU2不能进行操作。在前面已经提到java通过循环CAS操作和锁来实现原子操作。
循环CAS
CAS是CompareAndSwap的缩写,CAS的思想很简单:三个参数,一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。
对于上面的例子,我们可以将money定义为AtomicInteger类型,这是从java1.5开始在JUC并发包中提供的一些类来支持原子操作,而AtomicInteger是用原子方法更新int值的类。从下面的结果就可以看出getAndAdd()方法已经实现了原子操作,是线程安全的。
class IncreaseThread implements Runnable {
public static AtomicInteger money = new AtomicInteger(0);
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
money.getAndAdd(1);
}
System.out.println(money);
}
}
//输出结果
13569
28583
32408
44062
52724
60000
73230
80000
90000
100000
getAndAdd方法调用过程中有关compareAndSwap的相关源码
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 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;
}
CAS存在的三个问题
- ABA问题。CAS在操作值的时候会检查值有没有发生变化,例如读取内存地址V的值为A,在准备赋值的时候发现还是A,那能说明这个值没有发生改变吗?不能,因为完全可能有A--->B--->A,那么在CAS检查的时候不会检查到值发生了变化,但是实际却发生了变化,为了解决这个问题,可以在变量前面加入版本号,每次变化时将版本号+1,那么A--->B--->A就变为了1A--->2B--->3A。从java1.5开始,JUC包下提供了AtomicStampedReference来解决ABA问题,这个类的CompareAndSwap会先检查当前引用是否等于预期引用,如果相等,继续判断当前标志是否等于预期标志,如果都相等,将旧值更新为新值。
- 循环时间时间过长的问题。自旋如果长时间不能成功,这会给CPU带来很大的执行开销。
- 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
使用锁机制来实现原子操作
锁机制保证了只有获得锁的线程才能操作锁定的特定内存区域。JVM内部实现了很多锁,有偏向锁、轻量级锁和互斥锁,除了偏向锁,其他锁的实现方式都用到了循环CAS,即当一个一个线程想要进入同步块的时候使用循环CAS来获取锁,当它退出的时候使用循环CAS来释放锁。
参考文献
[1] Java并发编程的艺术
[2]https://blog.csdn.net/v123411739/article/details/79561458