Java并发编程实战 02Java如何解决可见性和有序性问题
Java并发编程实战 02Java如何解决可见性和有序性问题
摘要#
在上一篇文章当中,讲到了CPU缓存导致可见性、线程切换导致了原子性、编译优化导致了有序性问题。那么这篇文章就先解决其中的可见性和有序性问题,引出了今天的主角:Java内存模型(面试并发的时候会经常考核到)
什么是Java内存模型?#
现在知道了CPU缓存导致可见性、编译优化导致了有序性问题,那么最简单的方式就是直接禁用CPU缓存和编译优化。但是这样做我们的性能可就要爆炸了~。我们应该按需禁用。
Java内存模型是有一个很复杂的规范,但是站在程序员的角度上可以理解为:Java内存模型规范了JVM如何提供按需禁用缓存和编译优化的方法。
具体包括 volatile、synchronized、final三个关键字,以及六项Happens-Before规则。
volatile关键字#
volatile有禁用CPU缓存的意思,禁用CPU缓存那么操作数据变量时直接是直接从内存中读取和写入。如:使用volatile声明变量 volatile boolean v = false,那么操作变量v时则必须从内存中读取或写入,但是在低于Java版本1.5以前,可能会有问题。
在下面这段代码当中,假设线程A执行了write方法,线程B执行了reader方法,假设线程B判断到了this.v == true进入到了判断条件中,那么此时的x会是多少呢?
Copy
public class VolatileExample {
private int x = 0; private volatile boolean v = false; public void write() { this.x = 666; this.v = true; } public void reader() { if (this.v == true) { // 这里的x会是多少呢? } }
}
在1.5版本之前,该值可能为666,也可能为0;因为变量x并没有禁用缓存(volatile),但是在1.5版本以后,该值一定为666;因为Happens-Before规则。
什么是Happens-Before规则#
Happens-Before规则要表达的是:前面一个操作的结果对后续是可见的。如果第一次接触该规则,可能会有一些困惑,但是多去阅读几遍,就会加深理解。
1.程序的顺序性规则#
这条规则是指在一个线程中,按照程序顺序,前面的操作Happens-Before于后续的任意操作(意思就是前面的操作结果对于后续任意操作都是可以看到的)。就如上面的那段代码,按照程序的顺序:this.x = 666 Happens-Before于 this.v = true。
2.Volatile 变量规则#
这条规则指的是对一个Volatile变量的写操作,Happens-Before该变量的读操作。意思也就是:假设该变量被线程A写入后,那么该变量对于任何线程都是可见的。也就是禁用了CPU缓存的意思,如果是这样的话,那么和1.5版本以前没什么区别啊!那么如果再看一下规则3,就不同了。
3.传递性#
这条规则指的是:如果 A Happens-Before 于B,且 B Happens-Before 于 C。那么 A Happens-Before 于 C。这就是传递性的规则。我们再来看看刚才那段代码(我复制下来方便看)
Copy
public class VolatileExample {
private int x = 0; private volatile boolean v = false; public void write() { this.x = 666; this.v = true; } public void reader() { if (this.v == true) { // 读取变量x } }
}
在上面代码,我们可以看到,this.x = 666 Happens-Before this.v = true,this.v = true Happens-Before 读取变量x,根据传递性规则this.x = 666 Happens-Befote 读取变量x,那么说明了读取到变量this.v = true时,那么此时的读取变量x的指必定为666
假设线程A执行了write方法,线程B执行reader方法且此时的this.v == true,那么根据刚才所说的传递性规则,读取到的变量x必定为666。这就是1.5版本对volatile语义的增强。而如果在版本1.5之前,因为变量x并没有禁用缓存(volatile),所以变量x可能为0哦。
4.管程中锁的规则#
这条规则是指对一个锁的解锁操作 Happens-Before 于后续对这个锁的加锁操作。管程是一种通用的同步原语,在Java中,synchronized是Java里对管程的实现。
管程中的锁在Java里是隐式实现的。如下面的代码,在进入同步代码块前,会自动加锁,而在代码块执行完后会自动解锁。这里的加锁和解锁都是编译器帮我们实现的。
Copy
synchronized(this) { // 此处自动加锁
// x是共享变量,初始值 = 0
if (this.x < 12) {
this.x = 12;
}
} // 此处自动解锁
结合管程中的锁规则,假设x的初始值为0,线程A执行完代码块后值会变成12,那么当线程A解锁后,线程B获取到锁进入到代码块后,就能看到线程A的执行结果x = 12。这就是管程中锁的规则
5.线程的start()规则#
这条规则是关于线程启动的,该规则指的是主线程A启动子线程B后,子线程B能够看到主线程启动子线程B前的操作。
用HappensBefore解释:线程A调用线程B的start方法 Happens-Before 线程B中的任意操作。参考代码如下:
Copy
int x = 0; public void start() { Thread thread = new Thread(() -> { System.out.println(this.x); }); this.x = 666; // 主线程启动子线程 thread.start(); }
此时在子线程中打印的变量x值为666,你也可以尝试一下。
6.线程join()规则#
这条规则是关于线程等待的,该规则指的是主线程A等待子线程B完成(主线A通过调用子线程B的join()方法实现),当子线程B完成后,主线程能够看到子线程的操作,这里的看到指的是共享变量 的操作,用Happens-Before解释:如果在线程A中调用了子线程B的join()方法并成功返回,那么子线程B的任意操作 Happens-Before 于主线程调用子线程Bjoin()方法的后续操作。看代码比较容易理解,示例代码如下:
Copy
int x = 0; public void start() { Thread thread = new Thread(() -> { this.x = 666; }); // 主线程启动子线程 thread.start(); // 主线程调用子线程的join方法进行等待 thread.join(); // 此时的共享变量 x == 666 }
被忽略的final#
在1.5版本之前,除了值不可改变以外,final字段其实和普通的字段一样。
在1.5以后的Java内存模型中,对final类型变量重排进行了约束。现在只要我们的提供正确的构造函数没有逸出,那么在构造函数初始化的final字段的最新值,必定可以被其他线程所看到。代码如下:
Copy
class FinalFieldExample {
final int x;
int y;
static FinalFieldExample f;
public FinalFieldExample() {
x = 3; y = 4;
}
static void writer() {
f = new FinalFieldExample();
}
static void reader() {
if (f != null) { int i = f.x; int j = f.y; }
}
当线程执行reader()方法,并且f != null时,那么此时的final字段修饰的f.x 必定为 3,但是y不能保证为4,因为它不是final的。如果这是在1.5版本之前,那么f.x也是不能保证为3。
那么何为逸出呢?我们修改一下构造函数:
Copy
public FinalFieldExample() {
x = 3;
y = 4;
// 此处为逸出
f = this;
}
这里就不能保证 f.x == 3了,就算x变量是用final修饰的,为什么呢?因为在构造函数中可能会发生指令重排,执行变成下面这样:
Copy
// 此处为逸出
f = this;
x = 3;
y = 4;
那么此时的f.x == 0。所以在构造函数中没有逸出,那么final修饰的字段没有问题。详情的案例可以参考这个文档
总结#
在这篇文章当中,我一开始对于文章最后部分的final约束重排一直看的不懂。网上不断地搜索资料和看文章当中提供的资料我才慢慢看懂,反复看了不下十遍。可能脑子不太灵活吧。
该文章主要的核心内容就是Happens-Before规则,把这几条规则搞懂了就ok。
参考文章:极客时间:Java并发编程实战 02
个人博客网址: https://colablog.cn/
如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您
作者: Johnson木木
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
那么多物联网安全事件,我们从中学到了什么?
云栖号资讯:【点击查看更多行业资讯】在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来! 物联网为企业带来了前所未有的灵活性和功能性。更多的物联网设备有望帮助企业简化供应链运作,提高效率,降低现有流程中的成本,提高产品和服务质量,甚至为客户创造新的产品和服务。物联网将优化甚至彻底改革商业模式,使之变得更好。 物联网数据的大规模生成、收集和分析无疑将为企业提供巨大的利好,但通过不安全的网络和易受攻击的切入点,物联网设备很容易遭受攻击,这吸引了不少蠢蠢欲动的网络罪犯分子。 根据Gartner的数据,近20%的企业在过去三年中至少受到一次基于物联网的攻击。到2025年,全球联网设备预计将有750亿个,也就是说,网络安全漏洞和数据泄露的风险将在现在的基础上增加5倍。 因此,随着我们进入一个以物联网为主导的新时代,在部署多个连接设备时,必须重新审视企业面临的威胁,并将其纳入企业安全战略。以下是所有企业在制定网络防御计划中都应考虑到的物联网安全漏洞的三个案例——从对看似无利害关系的产品入侵,到彻头彻尾的恶意攻击。 1、最简单的连接设备也容易受到攻击 几乎所有去拉斯维加斯赌场的人都是赢少输...
- 下一篇
性能调优-python SDK 调优
python SDK python 和 java 或者和 GO ,在性能上来说都不是最好的,而且 python 无法支持多核的并发,只能跑在单核上的多线程。但是 oss 也提供了相应的方法提高多线程的文件吞吐; 初始化 在初始化时 python 有两个地方可以做调整 connect_timeout 可以增大客户端在数据读写过程中的超时时间,常用在客户端到 OSS 公网情况下上传大文件时增长时间,防止在公网抖动或者丢包情况下出现传输超时; # -*- coding: utf-8 -*- import oss2 # 阿里云主账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM账号进行API访问或日常运维,请登录RAM控制台创建RAM账号。 auth = oss2.Auth('<yourAccessKeyId
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2整合Redis,开启缓存,提高访问速度
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- CentOS7,CentOS8安装Elasticsearch6.8.6
- Red5直播服务器,属于Java语言的直播服务器
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- Hadoop3单机部署,实现最简伪集群