Java原子操作的两种方法及实现原理

原子操作的意思是"不可被中断的一个操作或者一系列操作"

实现方式

  1. 使用循环CAS实现原子操作
  2. 使用锁机制实现原子操作

首先我们看一个例子,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编程语言允许线程访问共享变量,为了确保共享变量能够被准确和一致性地更新,线程应该确保通过排它锁获得这个变量。

  1. volatile关键字可以保证此变量对所有线程都是可见的,即变量被修改的时候,其他线程会立刻知道变量值已经更新,下次操作的时候不会再从缓存中读取失效变量,而是从内存中读取更新的变量到缓存。
  2. 禁止指令重排。

那既然这样,在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存在的三个问题

  1. ABA问题。CAS在操作值的时候会检查值有没有发生变化,例如读取内存地址V的值为A,在准备赋值的时候发现还是A,那能说明这个值没有发生改变吗?不能,因为完全可能有A--->B--->A,那么在CAS检查的时候不会检查到值发生了变化,但是实际却发生了变化,为了解决这个问题,可以在变量前面加入版本号,每次变化时将版本号+1,那么A--->B--->A就变为了1A--->2B--->3A。从java1.5开始,JUC包下提供了AtomicStampedReference来解决ABA问题,这个类的CompareAndSwap会先检查当前引用是否等于预期引用,如果相等,继续判断当前标志是否等于预期标志,如果都相等,将旧值更新为新值。
  2. 循环时间时间过长的问题。自旋如果长时间不能成功,这会给CPU带来很大的执行开销。
  3. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。

使用锁机制来实现原子操作

锁机制保证了只有获得锁的线程才能操作锁定的特定内存区域。JVM内部实现了很多锁,有偏向锁、轻量级锁和互斥锁,除了偏向锁,其他锁的实现方式都用到了循环CAS,即当一个一个线程想要进入同步块的时候使用循环CAS来获取锁,当它退出的时候使用循环CAS来释放锁。

 

 

 

参考文献

[1] Java并发编程的艺术

[2]https://blog.csdn.net/v123411739/article/details/79561458

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值