Java 内存模型与线程
前言
并发是计算机发展的成就。(并发,同一时段发生;并行,同一时刻发生)
我们知道,早期计算机只能串行的进行运行(最古老的打孔)。而经过多年的发展,计算机可以“同时”做很多事情。但悲剧的是,因为CPU速度和其它设备之间的速度差别太大,比如磁盘IO、网络传输、数据库访问等等,如果不希望CPU在进行这些操作时一直处于等待的状态,就要充分压榨它的性能让它干别的事情。
目前在服务器端,衡量一个服务性能的高低好坏,每秒事务处理数(Transactions Per Second,TPS)是最重要的指标之一,它代表一秒内服务器端平均能响应的请求总数,而TPS的值与程序的并发能力又有非常密切的关系。对于计算量相同的程序,线程并发设计的越好,效率自然越高;反之,线程间的频繁阻塞甚至死锁,将会大大降低程序的并发能力。可见,并发是一个非常值得研究的问题。Java对并发进行了各种底层封装,使得程序员可以专注于业务逻辑而不必纠缠于这些复杂的细节。但是无论语言、中间件和框架如何优化,我们都不能100%保证它们能完美的解决并发问题,了解并发的内幕则是合格程序员的必经之路。
高效并发是《深入理解Java虚拟机》的最后一部分,将介绍虚拟机如何实现多线程、多线程之间由于共享和竞争数据而导致的一系列问题及解决方案。
一、硬件的效率与一致性
如果学过操作系统,这一块就很容易理解了。我们知道,计算机的执行速度是一个正三角模型,依次为:
CPU - 高速缓存 - 内存 - 外存
所以,要实现计算机并发执行多个任务和充分利用计算机CPU的性能就不是那么简单了。因为CPU和内存、外存的速度差别太大(跨越N个数量级),所以提出了高速缓存的概念。高速缓存是读写速度尽可能接近CPU运算速度的存储区域,它作为内存与CPU之间的缓冲:将运算需要使用到的数据复制到缓存中,让CPU进行运算,当运算计算后再从缓存同步到内存中,这样就无须等待缓慢的内存读写了。
引入高速缓存很好的解决了CPU与其它存储单元速度差异太大的问题,但同时也引入了新的问题——缓存一致性。在多CPU机器上,每个CPU都有自己的高速缓存,而它们又共享一个主内存,当多个CPU的运算任务都涉及内存的同一块区域时,就可能导致缓存不一致的情况,如果真是这样,那同步回内存的缓存以谁的数据为主呢?为了解决这个问题,需要各个CPU访问缓存时遵守一定的协议,比如MSI、MESI、MOSI等等。
整个过程可以用下图说明:

二、Java内存模型
首先最重要的一点是要知道为什么要有Java内存模型。
Java虚拟机规范定义了Java内存模型(Java Memory Model,JMM)来实现屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果。要抓住重点:屏蔽硬件差异,保证并发。而程序的功能就是数据流的交互,所以保证数据的快速、正确访问就是Java内存模型的核心。
在此之前,C/C++直接使用物理硬件(或者说是操作系统的内存模型),因此会导致不同平台、不同操作系统的差异:在一个平台上并发完全正常,到了另一个平台可能程序就会经常出错。因此还得针对不同的平台开发不同的C/C++版本。而Java为了实现平台无关性(Write Once,Run Anywhere),就定义了JMM。但是定义一个MM绝非易事:
(1) 必须足够严谨:这样才能保证Java的兵法操作不会产生歧义
(2) 必须足够宽松:使JVM的实现可以有足够的自由空间去利用硬件的各种特性(寄存器、高速缓存等)来获取更好的执行速度
1. 主内存与工作内存
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量和Java程序中的变量略有区别,它包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量和方法参数,因为它们是线程私有的,不会被共享,自然不存在竞争问题。(JVM堆中的数据,是多线程共享的)
Java内存模型规定了所有的变量存储在JVM的主内存中。每条线程还有自己的工作内存(类比高速缓存)。线程工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间的工作内存也是相互独立的,线程间变量值传递均需要主内存完成。线程、主内存、工作内存之间的关系如下图所示:

2. 内存间交互操作
关于主内存和工作内存之间的消息,主要是“主 - 工作”和“工作 - 主”,JMM定义了8种操作:
(1) lock:作用于主内存的变量,把一个变量标识为一条线程独占的状态
(2) unlock:作用于主内存的变量,把一个lock的变量解锁,可供其他线程使用
(3) read:作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存,供load动作使用
(4) load:作用于工作内存的变量,把read操作从主内存得到的变量值放入工作内存的变量副本中
(5) use:作用于工作内存的变量,把工作内存一个变量的值传递给执行引擎,当JVM遇到使用该变量的字节码指令会执行use操作
(6) assign:作用于工作内存的变量,把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行assign操作
(7) store:作用于工作内存的变量,把工作内存中一个变量的值传递给主内存中,供write动作使用
(8) write:作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中
This chapter requires login to view full content. You are viewing a preview.
Login to View Full Content