前言
并发处理的广泛应用是使得Amdahl定律代替摩尔定律成为计算机性能发展源动力的根本原因,也是人类“压榨”计算机运算能力的最有力武器。
现代计算机的处理器在追求运算速度的提升的同时,也朝着多核化方向发展。这个趋势越来越明显,已经成为现代处理器发展的主流趋势。
为什么我们希望计算机可以多做一些事呢?
原因是计算机的处理器的处理速度和计算机的存储设备的读写速度相差甚大,我们不希望处理器大部分时间都处于等待状态,这样会造成很大资源的浪费。
并发处理能力是衡量一台服务器好坏的主要指标,而服务端是java语言最擅长的领域之一,所以如何写好并发程序成为服务端程序开发的难点之一。下面通过介绍java内存模型来帮助理解java并发的开发。
硬件的效率与一致性
物理机遇到的并发问题与虚拟机中的情况有不少相似之处,物理机对并发的处理方案对于虚拟机的实现也有相当大的参考意义。
由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。
这样看似解决了处理器和内存速度的问题,但是仔细一想便会发现,每个cpu都有自己和缓存,那么他们之间的相同的变量如何保持一致?这个问题也就是缓存一致性问题(Cache Coherence)。每个处理器都有自己的高速缓存,而它们又共享同一主内存(Main Memory)。如图所示
当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致,如果真的发生这种情况,那同步回到主内存时以谁的缓存数据为准呢?为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、MESI(Illinois Protocol)、MOSI、Synapse、Firefly及Dragon Protocol等。
此外,处理器为了提高执行效率在运行程序的时候,执行指令的顺序并不一定会按照编码顺序来执行,但是会保证结果相同,这种现象称作乱序。
java内存模型
java虚拟机规范中试图定义一种java内存模型(java Memory Model,JMM)来屏蔽各种硬件和操作系统之间的差异,以实现让java程序在这个平台下都可以达到一致的内存访问效果。
主存与工作内存
java内存模型的主要目的是定义各个变量的的访问规则,即在虚拟机中将变量读取和存储到内存这样的底层细节。这里的变量和java程序中的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。
java内存模型做了如下的规定:
1.所有的变量都存储在主存中(可以与硬件的主存进行类比,虽然两者有相似之处,但是这里提到的主存是JVM中的一部分)
2.每一个线程都有自己的工作内存(可以与cpu cache进行类比),用来保存该线程用到的变量的主存拷贝副本
3.线程对变量的所有操作都只能通过本线程的工作变量进行,不可以直接读写主内存中的变量
4.不同的线程无法直接访问到其他线程的工作内存的变量,需要通过主内进行传递
线程、主内存、线程工作内存的关系如图
内存间的交互操作
java定义了以下8中操作来完成变量从主内存拷贝到工作内存、从工作内存同步回主内存。虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许有例外)
1.lock(锁定):作用于主内存的变量,它把一个变量表示为一个线程独占的状态。
2.unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
3.read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
4.load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
5.use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
6.assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
7.store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
8.write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
java内存模型规定了上述8中操作还要满足下面的规定:
1.不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。
2.不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
3.不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
4.一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use、store操作之前,必须先执行过了assign和load操作。
5.一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
6.如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
7.如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量。对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。
对于volatile型变量的特殊规则
关键词volatile可以说是java虚拟机提供的最轻量级的同步机制,它主要有两个作用:
1.保证此变量对所有的线程都可见
<1>use操作和read、load操作必须是是连续在一起出现,即在执行use操作之间必须先要执行load和read操作。这一点可以理解为当一个volatile变量在使用之前必须要从主内存中加载最新的值。
<2>assign操作和write、store操作必须是连续出现在一起,即执行assign操作之后必须执行store和write操作。这一点可以理解为当volatile变量的值被赋值之后就同步回主内存中。
上述的两点保证了volatile型变量的可见性
2.禁止虚拟机对指令的重排序优化
形成一个内存屏障,指令把修改volatile型变量的值同步到内存时,意味着所有之前的操作都已经执行完成。
这8种内存访问操作以及上述规则限定,再加上对volatile的一些特殊规定,就已经完全确定了Java程序中哪些内存访问操作在并发下是安全的。