摘要

在上一篇文章Java并发编程实战 01并发Bug的源头当中,讲到了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会是多少呢?

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。这就是传递性的规则。我们再来看看刚才那段代码(我复制下来方便看)

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 = truethis.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里是隐式实现的。如下面的代码,在进入同步代码块前,会自动加锁,而在代码块执行完后会自动解锁。这里的加锁和解锁都是编译器帮我们实现的。

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中的任意操作。参考代码如下:

    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()方法的后续操作。看代码比较容易理解,示例代码如下:

    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字段的最新值,必定可以被其他线程所看到。代码如下:

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

那么何为逸出呢?我们修改一下构造函数:

 public FinalFieldExample() {
x = 3;
y = 4;
// 此处为逸出
f = this;
}

这里就不能保证 f.x == 3了,就算x变量是用final修饰的,为什么呢?因为在构造函数中可能会发生指令重排,执行变成下面这样:

    // 此处为逸出
f = this;
x = 3;
y = 4;

那么此时的f.x == 0。所以在构造函数中没有逸出,那么final修饰的字段没有问题。详情的案例可以参考这个文档

总结

在这篇文章当中,我一开始对于文章最后部分的final约束重排一直看的不懂。网上不断地搜索资料和看文章当中提供的资料我才慢慢看懂,反复看了不下十遍。可能脑子不太灵活吧。

该文章主要的核心内容就是Happens-Before规则,把这几条规则搞懂了就ok。

参考文章:极客时间:Java并发编程实战 02

个人博客网址: https://colablog.cn/

如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您

Java并发编程实战 02Java如何解决可见性和有序性问题的更多相关文章

  1. java并发编程实战《一》可见性、原子性和有序性

    可见性.原子性和有序性问题:并发编程Bug的源头 核心矛盾:CPU.IO.内存三者之间的速度差异. 为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构.操作系统.编译程序都做出了贡献 ...

  2. Java并发编程实战 03互斥锁 解决原子性问题

    文章系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 摘要 在上一篇文章02Java如何解决可见性和有序性问题当中,我们解决了可见性和 ...

  3. Java并发编程实战 04死锁了怎么办?

    Java并发编程文章系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 Java并发编程实战 03互斥锁 解决原子性问题 前提 在第三篇 ...

  4. Java并发编程实战 05等待-通知机制和活跃性问题

    Java并发编程系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 Java并发编程实战 03互斥锁 解决原子性问题 Java并发编程实 ...

  5. 【java并发编程实战】-----线程基本概念

    学习Java并发已经有一个多月了,感觉有些东西学习一会儿了就会忘记,做了一些笔记但是不系统,对于Java并发这么大的"系统",需要自己好好总结.整理才能征服它.希望同仁们一起来学习 ...

  6. java并发编程实战《二》java内存模型

    Java解决可见性和有序性问题:Java内存模型 什么是 Java 内存模型? Java 内存模型是个很复杂的规范,可以从不同的视角来解读,站在我们这些程序员的视角,本质上可以理解为, Java 内存 ...

  7. Java并发编程实战——读后感

    未完待续. 阅读帮助 本文运用<如何阅读一本书>的学习方法进行学习. P15 表示对于书的第15页. Java并发编程实战简称为并发书或者该书之类的. 熟能生巧,不断地去理解,就像欣赏一部 ...

  8. java并发编程实战学习(3)--基础构建模块

    转自:java并发编程实战 5.3阻塞队列和生产者-消费者模式 BlockingQueue阻塞队列提供可阻塞的put和take方法,以及支持定时的offer和poll方法.如果队列已经满了,那么put ...

  9. Java并发编程实战---第六章:任务执行

    废话开篇 今天开始学习Java并发编程实战,很多大牛都推荐,所以为了能在并发编程的道路上留下点书本上的知识,所以也就有了这篇博文.今天主要学习的是任务执行章节,主要讲了任务执行定义.Executor. ...

随机推荐

  1. 108. Convert Sorted Array to Binary Search [Python]

    108. Convert Sorted Array to Binary Search Given an array where elements are sorted in ascending ord ...

  2. Consider defining a bean named 'authenticator' in your configuration.

    SpringBoot整合Shiro时出错: 异常日志: o.s.b.d.LoggingFailureAnalysisReporter: *************************** APPL ...

  3. ubuntu 下python出现pkg: error processing package *python* 解决之道

    1.linux有些自带程序很多是python写的,自带的python2也最好不要升级,不然会有很多问题 2.如果遇到 pkg: error processing package *python* (- ...

  4. 深入浅出C#结构体

    目录 1.应用背景 2.结构体解析 2.1.结构体存在栈中 2.2.结构体不需要手动释放 3.封装心跳包结构体 4.结构体静态帮助类 5.New出来的结构体是存在堆中还是栈中? 5.1.不带形参的结构 ...

  5. 拒绝了对对象 '***' (数据库 'BestSoftDB_P',架构 'sale')的 EXECUTE 权限。

    问题描述: 给普通用户授予读写权限,之后研发反映查询语句报错: nested exception is com.microsoft.sqlserver.jdbc.SQLServerException: ...

  6. Python第五章-内置数据结构04-字典

    Python 内置的数据结构 四.字典(dict) 字典也是 python 提供给我们的又一个非常重要且有用的数据结构. 字典在别的语言中有时叫关联数组.关联内存.Map等. 字典中存储的是一系列的k ...

  7. Pandas和Numpy的一些金融相关的操作(一)

    Pandas和Numpy的一些金融相关的操作 给定一个净值序列,求出最大回撤 # arr是一个净值的np.ndarray i = np.argmax( (np.maximum.acumulate(ar ...

  8. Redis系列(一):小试牛刀

    引言 随着互联网的高速发展,传统的关系数据库(如MySQL.Microsoft SQL Server等)已不能满足日益增长的业务需求,如商品秒杀.抢购等及时性非常强的功能,随着应用高并发的访问,会造成 ...

  9. 面试刷题27:程序员如何防护java界的新冠肺炎?

    背景 安全是软件设计的第二个非功能性需求,一般是当软件出现安全问题的时候才会得到重视. 最明显的比如 数据库用户信息和密码泄漏等: 数据加解密技术 单向加密 md5+salt值, 这个是软件设计中使用 ...

  10. 2019ICPC(银川) - Delivery Route(强连通分量 + 拓扑排序 + dijkstra)

    Delivery Route 题目:有n个派送点,x条双向边,y条单向边,出发点是s,双向边的权值均为正,单向边的权值可以为负数,对于单向边给出了一个限制:如果u->v成立,则v->u一定 ...