7.volatile:原子性

2018-06-13  本文已影响0人  xialedoucaicai

1.什么是原子性

原子性就是一个操作是不可再分割的,就像原子一样,可以理解为操作只有一步,一步已经是最小步骤了,自然就不能再分了。
典型的比如转账,其实分为两步,甲方少100,乙方多100,但这两步通常会加事务,强制变一步,这就是事务的原子性。其实所有的原子性/原子操作都是这个意思。
在Java中,只有基本数据类型的取值/赋值是原子操作。比如如下操作:

x = 10;         //语句1
y = x;         //语句2
x++;           //语句3
x = x + 1;     //语句4

只有语句1是原子操作,一步操作,将10赋值给x;语句2有两步操作:读取x的值,将x赋给y;语句3和语句4都有三步操作:读取x的值,加一,将新值赋给x。

2.volatile不能保证原子性

前面说过,volatile不能保证原子性,我们来看一个例子。我们启动10个线程,每个线程对共享变量执行一千次i++操作,最终看看打印结果。CountDownLatch保证10个线程执行完成后,再执行打印,这个是JUC的类,后面会讲到。
子线程,执行一千次i++

public class TestThread implements Runnable{
    //volatile不能保证原子性 atomic 美[əˈtɑ:mɪk]
    volatile int i = 0;
    CountDownLatch countDownLatch = null;
    
    public TestThread(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }
    
    @Override
    public void run() {
        for(int j=0;j<1000;j++){
            i++;
        }
        countDownLatch.countDown();
    }
}

主线程

public class Main {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(10);
        
        //volatile关键字,保证了可见性,但没有保证原子性
        TestThread thread = new TestThread(countDownLatch);
        for(int i=0;i<10;i++){
            new Thread(thread).start();         
        }
        
        //保证10个线程执行完毕,再打印最终结果
        countDownLatch.await();
        
        System.out.println(thread.i);
    }
}

正常执行结果应该是10000,但几乎每次运行结果都小于这个数,加了volatile,线程之间修改已经可见了,为啥还会出问题呢?

3.原子性问题分析

假设某一时刻i=10,线程A读取10到自己的工作内存,A对该值进行加一操作,但正准备将11赋给i时,由于此时i的值并未改变,B读取了主存的值仍为10到自己的工作内存,并执行了加一操作,正准备将11赋给i时,A将11赋给了i,由于volatile的影响,立即同步到主存,主存中的值为11,并使得B工作内存中的i失效,B执行第三步,虽然此时B工作内存中的i失效了,但是第三步是将11赋给i,对B来说,我只是赋值操作,并没有使用i这个动作,所以这一步并不会去刷新主存,B将11赋值给i,并立即同步到主存,主存中的值仍为11。虽然A/B都执行了加一操作,但主存却为11,这就是最终结果不是10000的原因。

4.如何保证原子性

那么对于i++这种非原子操作,我们如何让它变成原子操作呢?

  1. 可以通过synchronized关键字,因为i++是三步操作,多线程导致A在执行这三步操作期间被B干扰了,最终导致问题。我们对i++加上synchronized关键字,保证A在执行这三步操作时,不会被其他线程干扰,这样肯定就不会有问题了。
synchronized(this){
  i++;
}
  1. 可以使用java.util.concurrent.atomic包下面的封装的原子类来完成自增自减的操作,比如AtomicInteger。
    这些原子类是通过CAS(Compare And Swap)来实现原子操作的,CAS是CPU指令集的操作,是一个原子操作,速度极快。CAS的语义是“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少”。其实现思路其实就是乐观锁,以上面的分析为例,B从主存读取到i=10,B认为主存中的值得是10我的操作才生效,B在加一操作后准备将11赋值给i,去主存对比,发现主存的值变成了11,和B的预期值不一样,说明这个值肯定被别的线程改了,B放弃本次操作,更新预期值为11,进行下一次重试。下一次B加一后再去比对,发现预期值11和主存值11相等,才会真的将12赋值给i。
    由于CAS用CPU指令来实现无锁自增,所以,AtomicLong.incrementAndGet的自增比用synchronized的锁效率倍增。
    有关CAS更详细的资料可以参考这篇文章 非阻塞同步算法与CAS(Compare and Swap)无锁算法

5.最佳实践

由此例可以看出,volatile对于getAndOperate场景是无法胜任的,存在原子性问题。建议使用JUC的原子类来进行相关操作,同时如果有其他的保证原子性的场景,我们也可以利用CAS思想来自己写代码实现。

6.题外话

既然这里讲到了i++,就顺便推荐一篇讲i++和++i的文章Java第一课

上一篇下一篇

猜你喜欢

热点阅读