Java内存模型原子性原理及实例解析

 更新时间:2019年12月23日 14:53:30   作者:写代码的木公  
这篇文章主要介绍了Java内存模型原子性原理及实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下

这篇文章主要介绍了Java内存模型原子性原理及实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下

本文就具体来讲讲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,但是也有几次的结果并不是我们预期的。

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)

线程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机制,能保证操作真正的原子性。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

相关文章

  • java定义二维数组的几种写法(小结)

    java定义二维数组的几种写法(小结)

    下面小编就为大家带来一篇java定义二维数组的几种写法(小结)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-10-10
  • Java中BigDecimal类的add()的使用详解

    Java中BigDecimal类的add()的使用详解

    这篇文章主要介绍了Java中BigDecimal类的add()的使用详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-01-01
  • Spring实现HikariCP连接池的示例代码

    Spring实现HikariCP连接池的示例代码

    在SpringBoot 2.0中,我们使用默认连接池是HikariCP,本文讲一下HikariCP的具体使用,具有一定的参考价值,感兴趣的可以了解一下
    2021-08-08
  • Springboot工具类FileCopyUtils使用教程

    Springboot工具类FileCopyUtils使用教程

    这篇文章主要介绍了Springboot内置的工具类之FileCopyUtils的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2022-12-12
  • Presto自定义函数@SqlNullable引发问题详解

    Presto自定义函数@SqlNullable引发问题详解

    这篇文章主要为大家介绍了Presto自定义函数@SqlNullable引发问题详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • spring boot前后端交互之数据格式转换问题

    spring boot前后端交互之数据格式转换问题

    这篇文章主要介绍了spring boot前后端交互之数据格式转换,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-01-01
  • java 对称二叉树的判断

    java 对称二叉树的判断

    这篇文章主要介绍了java 对称二叉树的判断,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-02-02
  • 解读@EventListener工作原理

    解读@EventListener工作原理

    这篇文章主要介绍了@EventListener工作原理,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-08-08
  • Java 使用反射调用jar包中的类方式

    Java 使用反射调用jar包中的类方式

    这篇文章主要介绍了Java 使用反射调用jar包中的类方式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • 一文带你深入解析Java应用线程转储

    一文带你深入解析Java应用线程转储

    在Java应用运维和问题排查过程中,线程转储(Thread Dump)是一个非常重要的工具,它能够帮助我们了解JVM内部线程的运行状态,下面小编来和大家详细介绍一下它吧
    2025-04-04

最新评论