深入理解Java虚拟机(第二版) 第十二章:Java内存模型与线程

12.1 概述

因为CPU速度和其它设备之间的速度差别太大,比如磁盘IO、网络传输、数据库访问等等,如果不希望CPU在进行这些操作时一直处于等待的状态,就要充分压榨它的性能让它干别的事情。

目前在服务器端,衡量一个服务性能的高低好坏,每秒事务处理数(Transactions Per Second,TPS)是最重要的指标之一,它代表一秒内服务器端平均能响应的请求总数,而TPS的值与程序的并发能力又有非常密切的关系。对于计算量相同的程序,线程并发设计的越好,效率自然越高;反之,线程间的频繁阻塞甚至死锁,将会大大降低程序的并发能力。可见,并发是一个非常值得研究的问题。Java对并发进行了各种底层封装,使得程序员可以专注于业务逻辑而不必纠缠于这些复杂的细节。但是无论语言、中间件和框架如何优化,我们都不能100%保证它们能完美的解决并发问题,了解并发的内幕则是合格程序员的必经之路。

12.2 硬件的效率与一致性

我们知道,计算机的执行速度是一个正三角模型,依次为:

CPU - 高速缓存 - 内存 - 外存

所以,要实现计算机并发执行多个任务和充分利用计算机CPU的性能就不是那么简单了。因为CPU和内存、外存的速度差别太大(跨越N个数量级),所以提出了高速缓存的概念。高速缓存是读写速度尽可能接近CPU运算速度的存储区域,高速缓存作为内存与CPU之间的缓冲:将运算需要使用到的数据复制到缓存中,让CPU进行运算,当运算计算后再从缓存同步到内存中,这样就无须等待缓慢的内存读写了

引入高速缓存很好的解决了CPU与其它存储单元速度差异太大的问题,但同时也引入了新的问题——缓存一致性。在多CPU机器上,每个CPU都有自己的高速缓存,而它们又共享一个主内存,当多个CPU的运算任务都涉及内存的同一块区域时,就可能导致缓存不一致的情况,如果真是这样,那同步回内存的缓存以谁的数据为主呢?为了解决这个问题,需要各个CPU访问缓存时遵守一定的协议,比如MSI、MESI、MOSI等等。

整个过程可以用下图说明:
在这里插入图片描述

12.3 Java内存模型

Java虚拟机规范定义了Java内存模型(Java Memory Model,JMM)来实现屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台都能达到一致的并发效果。而程序的功能就是数据流的交互,所以保证数据的快速、正确访问就是Java内存模型的核心。

在此之前,C/C++直接使用物理硬件(或者说是操作系统的内存模型),因此会导致不同平台、不同操作系统的差异:在一个平台上并发完全正常,到了另一个平台可能程序就会经常出错。因此还得针对不同的平台开发不同的C/C++版本。而Java为了实现平台无关性(Write Once,Run Anywhere),就定义了JMM。

12.3.1 主内存与工作内存

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量和Java程序中的变量略有区别,它包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量和方法参数,因为它们是线程私有的,不会被共享,自然不存在竞争问题。(JVM堆中的数据,是多线程共享的)。

Java内存模型规定了所有的变量存储在JVM的主内存中。每条线程还有自己的工作内存(类比高速缓存)。线程工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间的工作内存也是相互独立的,线程间变量值传递均需要主内存完成。线程、主内存、工作内存之间的关系如下图所示:
在这里插入图片描述

12.3.2 内存间交互操作

关于主内存和工作内存之间的消息,主要是“主 - 工作”和“工作 - 主”,JMM定义了8种操作:

  • lock:作用于主内存的变量,把一个变量标识为一条线程独占的状态。
  • unlock:作用于主内存的变量,把一个lock的变量解锁,可供其他线程使用。
  • read:作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存,供load动作使用。
  • load:作用于工作内存的变量,把read操作从主内存得到的变量值放入工作内存的变量副本中。
  • use:作用于工作内存的变量,把工作内存一个变量的值传递给执行引擎,当JVM遇到使用该变量的字节码指令会执行use操作。
  • assign:作用于工作内存的变量,把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行assign操作。
  • store:作用于工作内存的变量,把工作内存中一个变量的值传递给主内存中,供write动作使用。
  • write:作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中。

从上面的规则,我们可以简单说几个例子说明一下。比如要把一个变量从主内存复制到工作内存,就要按顺序执行read和load操作;如果把一个变量从工作内存同步到主内存,就要按顺序执行store和write操作。

与此同时,JMM还规定了在执行上述8种基本操作时必须满足如下规则:

  • 不允许read和load、store和write操作之一单独出现,必须成对出现。即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起同步,主内存不接受。
  • 不允许一个线程丢弃assign操作,即变量在工作内存中改变了以后必须同步回主内存。
  • 不允许一个线程无原因(没有任何assign操作)就把数据从工作内存同步回主内存(因为是无用功嘛)。
  • 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,即对一个变量实现use和store操作之前,必须执行过assign或者load操作。
  • 一个变量在同一个时刻只允许一条线程对其lock,但lock可以被同一条线程重复执行多次,多次lock后也得有相同次数的unlock才能解锁。
  • 对一个变量执行lock操作,将会清空工作内存中这个变量的值,在执行引擎使用这个变量前,需要重新执行load或assign初始化变量的值。
  • unlock时必须保证是同一个线程在先前对这个变量执行过lock。
  • 对一个变量执行unlock之前,必须把此变量同步回主内存。

12.3.3 volatile型变量

volatile是轻量级同步机制, 它保证被修饰的变量在修改后立即列入主内存,使用变量前必须从主内存刷新到工作内存,这样就保证了所有线程的可见性。不存在隔离性。

对一个变量被定义为volatile之后,将具备两种特性:

  1. 保证此变量对所有线程的可见性
  2. 禁止指令重排序优化

特别说明

  1. 使用 volatile 的 static 变量,多线程并发 自增操作时,仍然有问题,因为,volatile,只是保证所有线程的可见性,2 个并发线程读到相同的变量值,自增操作后,会相互覆盖,丢失一次自增操作;(
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值