诡异并发三大恶人之有序性
云栖号:https://yqh.aliyun.com
第一手的上云资讯,不同行业精选的上云企业案例库,基于众多成功案例萃取而成的最佳实践,助力您上云决策!
上一节阿粉我和大家一起打到了并发中的恶人可见性和原子性,这一节我们继续讨伐三恶之一的有序性。
序、有序性的阐述
有序性为什么要探讨?因为 Java 是面向对象编程的,关注的只是最终结果,很少去研究其具体执行过程?正如上一篇文章在介绍可见性时描述的一样,操作系统为了提升性能,将 Java 语言转换成机器语言的时候,吩咐编译器对语句的执行顺序进行了一定的修改,以促使系统性能达到最优。所以在很多情况下,访问一个程序变量(对象实例字段,类静态字段和数组元素)可能会使用不同的顺序执行,而不是程序语义所指定的顺序执行。
正如大家所熟知那样,Java语言是运行在 Java 自带的 JVM(Java Virtual Machine) 环境中,在JVM环境中源代码(.class)的执行顺序与程序的执行顺序(runtime)不一致,或者程序执行顺序与编译器执行顺序不一致的情况下,我们就称程序执行过程中发生了重排序。
而编译器的这种修改是自以为能保证最终运行结果!因为在单核时代完全没问题;但是随着多核时代的到来,多线程的环境下,这种优化碰上线程切换就大大的增加了事故的出现几率!
好心办了坏事!
也就是说,有序性 指的是在代码顺序结构中,我们可以直观的指定代码的执行顺序, 即从上到下按序执行。但编译器和CPU处理器会根据自己的决策,对代码的执行顺序进行重新排序。优化指令的执行顺序,提升程序的性能和执行速度,使语句执行顺序发生改变,出现重排序,但最终结果看起来没什么变化(单核)。
有序性问题 指的是在多线程环境下(多核),由于执行语句重排序后,重排序的这一部分没有一起执行完,就切换到了其它线程,导致的结果与预期不符的问题。这就是编译器的编译优化给并发编程带来的程序有序性问题。
用图示就是:
阿粉小结:编译优化最终导致了有序性问题。
一、导致有序性的原因:
如果一个线程写入值到字段 a,然后写入值到字段 b ,而且b的值不依赖于 a 的值,那么,处理器就能够自由的调整它们的执行顺序,而且缓冲区能够在 a 之前刷新b的值到主内存。此时就可能会出现有序性问题。
例子:
1import java.time.LocalDateTime; 2 3/** 4 * @author :mmzsblog 5 * @description:并发中的有序性问题 6 * @date :2020年2月26日 15:22:05 7 */ 8public class OrderlyDemo { 9 10 static int value = 1; 11 private static boolean flag = false; 12 13 public static void main(String[] args) throws InterruptedException { 14 for (int i = 0; i < 199; i++) { 15 value = 1; 16 flag = false; 17 Thread thread1 = new DisplayThread(); 18 Thread thread2 = new CountThread(); 19 thread1.start(); 20 thread2.start(); 21 System.out.println("========================================================="); 22 Thread.sleep(6000); 23 } 24 } 25 26 static class DisplayThread extends Thread { 27 @Override 28 public void run() { 29 System.out.println(Thread.currentThread().getName() + " DisplayThread begin, time:" + LocalDateTime.now()); 30 value = 1024; 31 System.out.println(Thread.currentThread().getName() + " change flag, time:" + LocalDateTime.now()); 32 flag = true; 33 System.out.println(Thread.currentThread().getName() + " DisplayThread end, time:" + LocalDateTime.now()); 34 } 35 } 36 37 static class CountThread extends Thread { 38 @Override 39 public void run() { 40 if (flag) { 41 System.out.println(Thread.currentThread().getName() + " value的值是:" + value + ", time:" + LocalDateTime.now()); 42 System.out.println(Thread.currentThread().getName() + " CountThread flag is true, time:" + LocalDateTime.now()); 43 } else { 44 System.out.println(Thread.currentThread().getName() + " value的值是:" + value + ", time:" + LocalDateTime.now()); 45 System.out.println(Thread.currentThread().getName() + " CountThread flag is false, time:" + LocalDateTime.now()); 46 } 47 } 48 } 49}
运行结果:
从打印的可以看出:在 DisplayThread 线程执行的时候肯定是发生了重排序,导致先为 flag 赋值,然后切换到 CountThread 线程,这才出现了打印的 value 值是1,falg 值是 true 的情况,再为 value 赋值;不过出现这种情况的原因就是这两个赋值语句之间没有联系,所以编译器在进行代码编译的时候就可能进行指令重排序。
用图示,则为:
二、如何解决有序性
2.1、volatile
volatile 的底层是使用内存屏障来保证有序性的(让一个 CPU 缓存中的状态(变量)对其他 CPU 缓存可见的一种技术)。
volatile 变量有条规则是指对一个 volatile 变量的写操作, Happens-Before于后续对这个 volatile 变量的读操作。并且这个规则具有传递性,也就是说:
此时,我们定义变量 flag 时使用 volatile 关键字修饰,如:
1 private static volatile boolean flag = false;
此时,变量的含义是这样子的:
也就是说,只要读取到 flag=true; 就能读取到 value=1024;否则就是读取到flag=false; 和 value=1 的还没被修改过的初始状态;
但也有可能会出现线程切换带来的原子性问题,就是读取到 flag=false; 而value=1024 的情况;看过上一篇讲述[原子性]()的文章的小伙伴,可能就立马明白了,这是线程切换导致的。
2.2、加锁
此处我们直接采用Java语言内置的关键字 synchronized,为可能会重排序的部分加锁,让其在宏观上或者说执行结果上看起来没有发生重排序。
代码修改也很简单,只需用 synchronized 关键字修饰 run 方法即可,代码如下:
1 public synchronized void run() { 2 value = 1024; 3 flag = true; 4 }
同理,既然是加锁,当然也可以使用 Lock 加锁,但 Lock 必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。这点在使用的时候一定要注意!
使用该种方式加锁也很简单,代码如下:
1 readWriteLock.writeLock().lock(); 2 try { 3 value = 1024; 4 flag = true; 5 } finally { 6 readWriteLock.writeLock().unlock(); 7 }
好了,以上内容就是我对并发中的有序性的一点理解与总结了,通过这三篇文章我们也就大致掌握了并发中常见的可见性、有序性、原子性问题以及它们常见的解决方案。
最后
阿粉简单总结下三篇文章文章中使用的解决方案之间的区别:
云栖号:https://yqh.aliyun.com
第一手的上云资讯,不同行业精选的上云企业案例库,基于众多成功案例萃取而成的最佳实践,助力您上云决策!
原文发布时间:2020-03-04
本文作者:鸭血粉丝
本文来自:“Java极客技术”,了解相关信息可以关注“Java极客技术”
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
代码演示Mybatis-Generator 扩展自定义生成
Mybatis-Generator 可自动生成Model、Dao、Mapper代码,但其自带生成的代码存在以下问题: 生成的注释不是我们想要的,我们期望的是根据数据库表、字段生成不同的注释; 分页代码生成缺失,每个公司的分页方式不同,尤其是老久项目或已发布API,不能随意变动,那么如何自适应分页代码生成; Mapper.xml没有group by相关代码生成; 重复生成代码时,Mapper.xml并不是覆盖原代码;而是对内容进行了追加; 序列化,mybatis-generator内置了SerializablePlugin,但仅对Model,并没有对 Example序列化,在一些开发中是不够的; 对Service Layer代码没有生成。 实际上,mybatis-generator提供了PluginAdapter供我们来继承,进行个性化的一些扩展(Plugin的相关内容是阅读本文的前置条件)如果不熟悉的同学请自行补充,本文不对其进行相关介绍。同时,本文不可能涵盖所有业务所需的扩展点,基本样板已有,可参考本文代码继续进行扩展。 1、注释的自定义生成 根据数据库表或字段的COMMENT生成注...
- 下一篇
JeeSite v4.1.8 更新,OAuth2,微信集成,Flowable
v4.1.8 2020-3-3 新增 新增 OAuth2 第三方登录模块(快速登录:码云、QQ、微信、等等) 新增 账号注册 界面的功能示例(看演示:http://demo.jeesite.com) 新增微信模块,实现微信消息推送、微信公众号与JeeSite绑定登录、小程序与企业微信接口引入 部门树接口 isLoadUser 新增 lazy 懒加载,异步加载,点击再加载 新增全空格的前端验证,验证名称:isBlank BPM 新增 BpmTreeEntity 树表支持 BPM 表单节点选择支持选择连线,用于连线事件设定 Beetl 添加 hasRole 角色权限函数 Excel 工具支持 BigDecimal 类型 新增 inputmask 插件金额使用例子 新增 editGrid 列表选择组件的例子 Cloud 新增 代码生成模板 优化 消息列表的消息内容查询改为模糊查询 DataGrid 优化,点击列表查询后,定向到第一页 实体类分离出接口 BaseEntityApi、DataEntityApi、TreeEntityApi 修改据库表字段 password 长度为 200,第三方系...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
-
Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8编译安装MySQL8.0.19
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
推荐阅读
最新文章
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- Hadoop3单机部署,实现最简伪集群
- CentOS6,CentOS7官方镜像安装Oracle11G
- CentOS7安装Docker,走上虚拟化容器引擎之路
- SpringBoot2全家桶,快速入门学习开发网站教程
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库