在并发编程中有两个问题需要解决:线程间的通信实现和线程间的同步控制。针对这两个问题,命令式编程中 现有两种并发模型可用—共享内存模型和消息传递模型,那么这两种模型分别是如何解决并发编程的两个问题呢?
-
共享内存模型:线程之间通过共享内存实现通信;通过显示指定某个方法或某段代码需要在线程之间互斥执行实现同步。
-
消息传递模型:线程之间通过发送消息实现通信;通过消息发送在而接收在后控制执行顺序来实现同步。
Java并发采用的是共享内存模型。
1. Java 内存模型 (JMM)
Java内存模型是JavaTM语言规范的一部分,主要包括以下内容:
1.1 Java内存模型通过控制主内存与每个线程本地内存之间的交互来控制了Java线程间的通信
因为Java并发模型采用的是共享内存模型,所以它通过控制一个线程对共享变量的写入何时对另一个线程可见来实现Java线程之间的通信。从抽象的角度讲,Java内存模型定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。一个线程A读写本地内存的共享变量副本之后写会主内存,另一个线程B再去主内存读取共享变量的新值。
本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
1.2 Java内存模型禁止特定类型的编译器重排序,并通过在Java编译器在生成指令序列期间插入特定类型的内存屏障阻止处理器指令重排序
在执行程序时,为了提高CPU的使用率,编译器和处理器常常会对指令多重排序。重排序可以分为以下三类:
1)编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2)指令集并行的重排序:现代处理器采用指令级并行技术来将多条指令重叠执行。如果数据不存在依赖
性,处理器可以改变语句对应机器指令的执行顺序。
3)内存系统的重排序:由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是乱序的。
以上这些重排序都遵循as-if-serial原则,即不管怎么重排序,都能保证单线程条件下程序执行结果不会被改变,但是多线程条件下程序的执行结果就不能保证了。Java内存模型为了保证多线程程序执行的结果,禁止了特定类型的编译器重排序,并通过在Java编译器生成指令序列期间插入特定类型的内存屏障阻止处理器指令重排序。
1.2.1 Happens-before 规则
Java内存模型禁止特定类型编译器重排序规则归纳之后其实就是Happens-before规则,内容如下:
- 程序顺序性规则:一个线程中的每个操作,Happens-before于线程中的任意后续操作。
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
这里看Happens-before规则有点儿像废话,但是这些规则是给编译器看的,我们了解这些只是为了知道Java内存模型是怎么约束编译器的重排序的。
1.3 Java内存模型保证了正确同步的多线程程序内存的一致性
Java内存模型保证:如果程序是正确同步的,程序的执行将具有顺序一致性—即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。直白一点儿,就是程序员正确的使用同步原语(synchronized、volatile、final),Java内存模型就能保证执行结果的正确性。后面章节会逐个学习这些同步原语的原理,以期理解它们是如何保证内存的一致性。
3. 小结
本章主要了解了Java的并发模型核心内容-Java内存模型。对于程序员来讲,写出可用的并发编程只需要正确的使用Java提供的并发关键字就可以,不需要知道Java内存模型的内容究竟是什么,Java内存模型是给JVM的厂商使用的,是用来规范计算机和编译器的。我们现在了解这些知识是为了系统的认识Java并发,理解它的原理和实现。简单来说,Java内存模型就是Java这门语言解决并发编程问题的指导规范。
参考文献 《Java并发编程的艺术》