美文网首页
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:原子性

    1.什么是原子性 原子性就是一个操作是不可再分割的,就像原子一样,可以理解为操作只有一步,一步已经是最小步骤了,自...

  • Objective-c 属性存取器@property默认属性

    原子性 nonatomic 非原子性访问 atomic 原子性访问,默认值 读写权限 readonly 只读 re...

  • Java 多线程三大核心

    目录:一、 原子性二、可见性三、顺序性 一、 原子性 Java 的原子性就和数据库事务的原子性差不多,一个操作中要...

  • IOS面试题(类相关) --- 属性关键字

    问题1: 描述下属性关键字 原子性: 原子性(atomic) 默认 非原子性(nonatomic) 详细可以参考...

  • C++原子性操作,volatile关键字

    原子性操作 原子性操作比加锁访问临界资源更加高效。现代CPU支持一些原子性操作 使用原子性操作实现无锁队列。。。参...

  • mysql 事务机制

    一、事务四大特性(ACID)原子性、一致性、隔离性、持久性 原子性(Atomicity):原子性是指,事务包含的所...

  • Java自学-多线程 原子访问

    多线程 原子访问 步骤 1 : 原子性操作概念 所谓的原子性操作即不可中断的操作,比如赋值操作 原子性操作本身是线...

  • java并发编程要点

    Java并发问题主要有三个核心概念:原子性,可见性,顺序性。 原子性 并发问题的原子性的概念和数据库事务的原子性是...

  • 原子性

    原子性: 要么全部执行成功要么全部执行失败

  • JAVA线程基础回顾及内存模型(二)

    原子性 可见性和有序性 原子性(Atomicity):由JMM直接保证原子性变量操作在上节的read\load,s...

网友评论

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

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