Java内存模型之原子性问题


本博客系列是学习并发编程过程当中的记录总结。因为文章比较多,写的时间也比较散,因此我整理了个目录贴(传送门),方便查阅。html

并发编程系列博客传送门java


前言

以前的文章中讲到,JMM是内存模型规范在Java语言中的体现。JMM保证了在多核CPU多线程编程环境下,对共享变量读写的原子性、可见性和有序性。spring

本文就具体来说讲JMM是如何保证共享变量访问的原子性的。编程

原子性问题

原子性是指:一个或多个操做,要么所有执行且在执行过程当中不被任何因素打断,要么所有不执行。多线程

下面就是一段会出现原子性问题的代码:并发

public class AtomicProblem {

    private static Logger logger = LoggerFactory.getLogger(AtomicProblem.class);
    public static final int THREAD_COUNT = 10;

    public static void main(String[] args) throws Exception {
        BankAccount  sharedAccount = new BankAccount("account-csx",0.00);
        ArrayList<Thread> threads = new ArrayList<>();
        for (int i = 0; i < THREAD_COUNT; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 1000 ; j++) {
                        sharedAccount.deposit(10.00);
                    }
                }
            });
            thread.start();
            threads.add(thread);
        }
        for (Thread thread : threads) {
            thread.join();
        }
        logger.info("the balance is:{}",sharedAccount.getBalance());
    }


    public static class BankAccount {
        private String accountName;

        public double getBalance() {
            return balance;
        }

        private double balance;

        public BankAccount(String accountName, double balance){
            this.accountName = accountName;
            this.balance =balance;
        }
        public double deposit(double amount){
            balance = balance + amount;
            return balance;
        }
        public double withdraw(double amount){
            balance = balance - amount;
            return balance;
        }
        public String getAccountName() {
            return accountName;
        }
        public void setAccountName(String accountName) {
            this.accountName = accountName;
        }
    }
}

上面的代码中开启了10个线程,每一个线程会对共享的银行帐户进行1000次存款操做,每次存款10块,因此理论上最后银行帐户中的钱应该是10 * 1000 * 10 = 100000块。我执行了屡次上面的代码,不少次最后的结果的确是100000,可是也有几回的结果并非咱们预期的。ide

14:40:25.981 [main] INFO com.csx.demo.spring.boot.concurrent.jmm.AtomicProblem - the balance is:98260.0

出现上面结果的缘由就是由于下面的操做并非原子操做,其中的balance是一个共享变量。在多线程环境下可能会被打断。学习

balance = balance + amount;

上面的赋值操做被分为多步执行完成,下面简单解析下两个线程对balance同时加10的过程(模拟存款过程,假设balance的初始值仍是0)this

线程1从共享内存中加载balance的初始值0到工做内存
线程1对工做内存中的值加10

//此时线程1的CPU时间耗尽,线程2得到执行机会

线程2从共享内存中加载balance的初始值到工做内存,此时balance的值仍是0
线程2对工做内存中的值加10,此时线程2工做内存中的副本值是10
线程2将balance的副本值刷新回共享内存,此时共享内存中balance的值是10

//线程2CPU时间片耗尽,线程1又得到执行机会
线程1将工做内存中的副本值刷新回共享内存,可是此时副本的值仍是10,因此最后共享内存中的值也是10

上面简单模拟了一个原子性问题致使程序最终结果出错的过程。线程

JMM对原子性问题的保证

自带原子性保证

在Java中,对基本数据类型的变量读取赋值操做是原子性操做。

a = true;  //原子性
a = 5;     //原子性
a = b;     //非原子性,分两步完成,第一步加载b的值,第二步将b赋值给a
a = b + 2; //非原子性,分三步完成
a ++;      //非原子性,分三步完成

synchronized

synchronized能够保证操做结果的原子性。synchronized保证原子性的原理也很简单,由于synchronized能够防止多个线程并发执行一段代码。仍是用上面存款的场景作列子,咱们只须要将存款的方法设置成synchronized的就能保证原子性了。

public synchronized double deposit(double amount){
     balance = balance + amount; //1
     return balance;
 }

加了synchronized后,当一个线程没执行完deposit这个方法前,其余线程是不能执行这段代码的。其实咱们发现synchronized并不能将上面的代码1编程原子性操做,上面的代码1仍是有可能被中断的,可是即便被中断了其余线程也不能访问共享变量balance,当以前被中断的线程继续执行时获得的结果仍是正确的。

所以synchronized对原子性问题的保证是从最终结果上来保证的,也就是说它只保证最终的结果正确,中间操做的是否被打断无法保证。这个和CAS操做须要对比着看。

Lock

public double deposit(double amount) {
    readWriteLock.writeLock().lock();
    try {
        balance = balance + amount;
        return balance;
    } finally {
        readWriteLock.writeLock().unlock();
    }
}

Lock锁保证原子性的原理和synchronized相似,这边不进行赘述了。

原子操做类型

public static class BankAccount {
    //省略其余代码
    private AtomicDouble balance;

    public double deposit(double amount) {
        return balance.addAndGet(amount);
    }
    //省略其余代码
}

JDK提供了不少原子操做类来保证操做的原子性。原子操做类的底层是使用CAS机制的,这个机制对原子性的保证和synchronized有本质的区别。CAS机制保证了整个赋值操做是原子的不能被打断的,而synchronized值能保证代码最后执行结果的正确性,也就是说synchronized能消除原子性问题对代码最后执行结果的影响。

简单总结

在多线程编程环境下(不管是多核CPU仍是单核CPU),对共享变量的访问存在原子性问题。这个问题可能会致使程序错误的执行结果。JMM主要提供了以下的方式来保证操做的原子,保证程序不受原子性问题的影响。

  • synchronized机制:保证程序最终正确性,是的程序不受原子性问题的影响;
  • Lock接口:和synchronized相似;
  • 原子操做类:底层使用CAS机制,能保证操做真正的原子性。
相关文章
相关标签/搜索