美文网首页
7.volatile:原子性

7.volatile:原子性

作者: xialedoucaicai | 来源:发表于2018-06-13 10:48 被阅读0次

    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第一课

    相关文章

      网友评论

          本文标题:7.volatile:原子性

          本文链接:https://www.haomeiwen.com/subject/hfiosftx.html