推荐无广告版本:Java 并发编程[1] – Java 内存模型
CPU
做其他运算。这样,并发编程能够提高 CPU
(即使是单核) 的利用率。在并发编程中,有两种选择:多进程 or 多线程。学过操作系统的都知道它们的区别。只简单的用一句话描述它们的区别:++进程是资源分配的基本单位,而线程是调度的基本单位++。
而在 Java 中,并发编程中最多使用的是:多线程编程。当然,Java 也可以进行多进程编程,使用 Runtime.exec()
创建一个进程(底层使用 fork()
实现,可以用它来执行脚本)。但是,使用的不多。以下的内容都是针对的 Java 多线程。
下面认识两种并发模型(这两种模型同样适用于多进程)。
在开始之前,我们需要认识到并发编程两个关键问题:通信 和 同步。通信,说明的线程间的交流。因为能够通信的线程在并发中才有意义(针对大部分线程来说);同步,讲究的是线程的执行顺序。在一些应用中,线程的执行往往有先后要求的。比如,写线程写完后,读线程才能开始读。经典的 生产者-消费者 问题,就一种同步问题。下面的两种并发模型,针对这两个关键问题有不同的要求。
基于 共享内存 的并发模型中,所有的线程有一个共享的内存空间。
基于 消息 的并发模型中,线程间可以进行消息传递。
在 Java 中,使用的是基于共享内存的编程模型。Java 中线程使用共享内存来通信,而 Java 虚拟机(JVM)为程序员保证共享内存对线程的可见性。至于,线程的同步就需要程序员自己控制了。但这里需要注意的是,虽然 JVM 提供了内存可见性的支持,但是是有条件的。也就是说,我们必须在其描述的环境下才能获取保证的内存可见性。
Java 内存模型(JMM)就是 JVM 对内存可见性实现的综述。所以,要想深入学习 Java 并发编程,了解 JMM 是很有必要的。
内存可见性保证了某一时刻各个线程所见到的共享内存是一致的。如果之前没有并发编程经验,可以会为此感到惊讶。因为,在单线程中无论如何也不会发生同一个变量有不同值的情况。为什么多线程会出现这种情况,认识了内存可见性的障碍就豁然开朗了。
内存可见性有两大障碍:本地内存(缓存) 和 重排序。
花开两朵,各表一枝。先看,本地内存,其实它是 JMM 中的一个抽象概念(见下图)。为了方便理解,暂且把它视为 缓存。缓存这一概念在计算机中非常常见。比如,为了缓和 CPU 与 Memory 的速度差异出现了 Cache,它就是一种缓存机制。想一想 Cache 的作用,就不难理解 JMM 中的本地内存,它也是为了提高 Java 性能而提出了。Java 中每一个线程都有自己本地内存。线程要读取变量时,先要在自己的本地内存中查找。如果命中了直接使用本地内存中的,否则,再去内存中查找。就像你现在想到的一样,它虽提高了性能,但会像 Cache 那样读到赃的数据(一个线程正要读取变量a时,恰巧被挂起了而去执行另一个线程。巧的是,该线程要修改变量a。这样前面线程恢复时,由于受缓存的影响读到的变量a的值可能不是最新的)。所以,本地内存给内存可见性带来了极大的障碍。但是,为了性能,我们又不能丢弃它。怎么解决呢?(暂且不说,想想 Cache 是怎么解决类似问题的)
再看,重排序。可能这个概念我们比较陌生。其实很简单,说白了就是打破原有的顺序。打破什么的顺序呢?一是 Java 代码的顺序,另一个是指令的顺序。这两者有什么区别呢?前者是在编译期间完成的(编译器重排序),而后者是在执行时完成的(处理器重排序)。为什么重排序呢?还是,为了提高 Java 的性能。比如,一些编译优化手段与代码顺序有关、并行指令优化技术 等等。
再具体看重排序对内存可见性的影响。考虑下面代码,现在有两个线程共享 ReorderExample
的实例:线程A率先调用 writer()
方法,线程B紧接着调用 read()
方法。那线程B 一定能看到线程A对 a
的修改吗?答案是:不一定。其实,我们通过了解了 本地内存 就能解释它的原因。现在,忽略缓存的影响,那一定能看到 a
修改后的值吗?答案还是:不一定。这是因为受 重排序 的影响。重排序后,执行顺序可能变成下面图中那样。代码2)先于代码1),代码3、4)也先于代码1)。如此即使无缓存影响,也不能看到对 a
的修改。怎么解决呢?禁止部分重排序。(具体内容按下不表)
class ReorderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1; // 1
flag = true; // 2
}
public void read() {
if (flag){ // 3
int t = a * a; // 4
}
}
}
上面介绍了内存可见性的两大障碍。而 JMM 正是对这两大障碍开刀,为程序员提高透明的内存可见性保障。所以,对 JMM 我们有以下两种解释:
看到这里,是不是仍对 JMM 有云里雾里的感觉?没关系,Java 大佬考虑到了我们。再 JSR-133 中使用 happens-before
的概念阐述操作之间的内存可见性。在 JMM 中,如果一个操作只从的结果需要另一个操作可见,那么这两个操作之间必须要存在 happens-before
关系(这里提到的两个操作既可以是一个线程之内,也可以是在不同线程之间)。
下图完美阐述了 JMM 与 happens-before 的关系。
happens-before 可以说是 JMM 的封装。一个 happens-before 规则对应于一个或多个编译器和处理器重排序规则。对于 Java 程序员来说,happens-before 规则简单易懂,它避免我们为了理解 JMM 提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现方法。
下一篇,让我们开始认识 happens-before。