Java内存模型之原子性问题
本博客系列是学习并发编程过程中的记录总结。由于文章比较多,写的时间也比较散,所以我整理了个目录贴(传送门),方便查阅。
前言
之前的文章中讲到,JMM是内存模型规范在Java语言中的体现。JMM保证了在多核CPU多线程编程环境下,对共享变量读写的原子性、可见性和有序性。
本文就具体来讲讲JMM是如何保证共享变量访问的原子性的。
原子性问题
原子性是指:一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。在Java中当我们讨论一个操作具有原子性问题是一般就是指这个操作会被线程的随机调度打断。
下面就是一段会出现原子性问题的代码:
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 getBalance() {
return balance;
}
public synchronized double deposit(double amount){
balance = balance + amount; //1
return balance;
}
加了synchronized后,当一个线程没执行完deposit
这个方法前,其他线程是不能执行这段代码的。其实我们发现synchronized并不能将上面的代码1编程原子性操作,上面的代码1还是有可能被中断的,但是即使被中断了其他线程也不能访问共享变量balance
,当之前被中断的线程继续执行时得到的结果还是正确的。
因此synchronized对原子性问题的保证是从最终结果上来保证的,也就是说它只保证最终的结果正确,中间操作的是否被打断没法保证。这个和CAS操作需要对比着看。
PS:对于上面的getBalance
方法大家可能会有点疑惑:只读操作为什么还要加上synchronized关键字。其实这边加上synchronized关键字的目的是为了保证balance变量的可见性,进入synchronized代码块每次都会去从主内存中读取最新值。
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能消除原子性问题对代码最后执行结果的影响。
PS:JVM中的CAS操作是利用处理器提供的CMPXCHG指令实现的。
简单总结
在多线程编程环境下(无论是多核CPU还是单核CPU),对共享变量的访问存在原子性问题。这个问题可能会导致程序错误的执行结果。JMM主要提供了如下的方式来保证操作的原子,保证程序不受原子性问题的影响。
- synchronized机制:保证程序最终正确性,是的程序不受原子性问题的影响;
- Lock接口:和synchronized类似;
- 原子操作类:底层使用CAS机制,能保证操作真正的原子性。
Java内存模型之原子性问题的更多相关文章
- 「跬步千里」详解 Java 内存模型与原子性、可见性、有序性
文题 "跬步千里" 主要是为了凸显这篇文章的基础性与重要性(狗头),并发编程这块的知识也确实主要围绕着 JMM 和三大性质来展开. 全文脉络如下: 1)为什么要学习并发编程? 2) ...
- java内存模型的原子性、可见性、有序性与指令重排序
在并发编程中,我们通常会遇到以下三个概念:原子性.可见性和有序性.我们先看具体看一下这三个概念: 1.原子性 操作时不可分割的比如a=0,此操作不可分割,而++a,实际上是a=a+1,为两个操作.想将 ...
- 并发编程-Java内存模型
将之前看过的关于并发编程的东西总结记录一下,本文简单记录Java内存模型的相关知识. 1. 并发编程两个关键问题 并发编程中,需要处理两个关键问题:线程之间如何通信及线程之间如何同步. (1)在命令式 ...
- Java内存模型与线程(一)
Java内存模型与线程 TPS:衡量一个服务性能的标准,每秒事务处理的总数,表示一秒内服务端平均能够响应的总数,TPS又和并发能力密切相关. 在聊JMM(Java内存模型)之前,先说一下Java为什么 ...
- 浅析 Java 内存模型
文章转载于 飞天小牛肉 的 <「跬步千里」详解 Java 内存模型与原子性.可见性.有序性>.<JMM 最最最核心的概念:Happens-before 原则> 1. 为什么要学 ...
- Java内存模型JMM 高并发原子性可见性有序性简介 多线程中篇(十)
JVM运行时内存结构回顾 在JVM相关的介绍中,有说到JAVA运行时的内存结构,简单回顾下 整体结构如下图所示,大致分为五大块 而对于方法区中的数据,是属于所有线程共享的数据结构 而对于虚拟机栈中数据 ...
- Java-JUC(二):Java内存模型可见性、原子性、有序性及volatile具有特性
1.Java HotSpot JVM运行时数据区 Java内存模型即Java Memory Model,简称JMM.JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式.JVM是整 ...
- Java内存模型(三)原子性、内存可见性、重排序、顺序一致性、volatile、锁、final
一.原子性 原子性操作指相应的操作是单一不可分割的操作.例如,对int变量count执行count++d操作就不是原子性操作.因为count++实际上可以分解为3个操作:(1)读取变量co ...
- JVM学习(3)——总结Java内存模型
俗话说,自己写的代码,6个月后也是别人的代码……复习!复习!复习!涉及到的知识点总结如下: 为什么学习Java的内存模式 缓存一致性问题 什么是内存模型 JMM(Java Memory Model)简 ...
随机推荐
- 2019-9-25:渗透测试,基础学习,Hydra BP爆破,js基本知识,banner信息收集笔记
使用BP和Hydra爆破相关的服务hydra:九头蛇,开源的功能强大的爆破工具,支持的服务有很多,使用hydra爆破c/s结构的服务,使用bp爆破web登陆窗口爆破需要的几个条件,爆破工具+字典字典: ...
- 推荐几个不错的console调试技巧
在我们的日常前端开发中,使用最频繁的莫过于使用console.log在浏览器的控制台中打印出我们需要调试的信息,但是大部分人可能跟之前的我一样,没有意识到其实console除了log方法以外,还有很多 ...
- 一图读懂Spring Core,Spring MVC, Spring Boot,Spring Cloud 的关系与区别
Spring框架自诞生到现在,历经多次革新,形成了多种不同的产品,分别应用于不同的项目中,为了帮助自己理解这些产品之间的关系,特此整理此图,以便自己记忆和复习.
- ehcache同步原理
最近研究ehcache同步时发现一个问题: 现有A.B两个服务器,由A服务器向B服务器同步信息,采用RMI方式手动方式进行同步 配置信息如下: <?xml version="1.0&q ...
- 简单地认识一下 HTML
简单复盘一下 HTML. 1.HTML 什么是 HTML?HTML 是 Hyper Text Markup Language 的简写,译成中文是「超文本标记语言」. 顾名思义,超文本,就是不止于文本, ...
- 【开发工具 - Android Studio】之AndroidStudio使用笔记
一.关闭自动更新: 问题:刚刚安装Android Studio的童鞋可能会遇到这样一个问题:Android Studio在打开的时候一直在下载一些东西,浪费很多时间,而且最终大多都会显示下载失败等等, ...
- 简单ORM的实现
简单的orm实现 我们在使用各种框架的时候,关于数据库这方面的使用,框架给我们提供了很好的封装,这个就是orm 关系映射 orm的底层无非就是做了关系映射 数据库的表(table) --> 类( ...
- vue如何引入图片地址
我们在用vue时储存图片时,一般把图片放在两种文件下,一个是static文件夹下,另外一个是assets文件夹下. 下面总体说一下这两个的区别及正确的引用方式: static是放不会变动的图片(或文件 ...
- 磁盘格式化、磁盘挂载、手动增加swap空间 使用介绍
第4周第2次课(4月10日) 课程内容: 4.5/4.6 磁盘格式化4.7/4.8 磁盘挂载4.9 手动增加swap空间 4.5/4.6 磁盘格式化 [root@jimmylinux-002 ~]# ...
- linux任务计划cron、chkconfig工具、systemd管理服务、unit和target介绍
第8周第1次课(5月14日) 课程内容: 10.23 linux任务计划cron10.24 chkconfig工具10.25 systemd管理服务10.26 unit介绍10.27 target介绍 ...