[TOC]
由于CPU的缓存一致性和乱序执行优化, 我们会知道在多核高并发下需要额外做很多事情, 才能保证程序执行的预期.
Java虚拟机是如何解决这些问题?
为了屏蔽掉各种硬件和操作系统之间内存访问的各种差异, 以实现Java程序在各种平台下都能达到一致的并发效果, Java虚拟机规范中定义了Java内存模型(Java Memory Model, JMM), Java内存模型是一种规范, 它规范了Java虚拟机与计算机内存是如何协同工作的, 它规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值, 以及在必须时如何同步的访问共享变量。
Java内存模型规范
规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值, 以及在必须时如何同步的访问共享变量。
在明确了Java内存模型是做什么的后, 下面我们具体介绍Java内存模型。
首先明确JVM内存分配的概念
首先需要明白上图中两个JVM分配内存的概念, 一个是堆Heap
, 一个是栈Stack
。
Java里的堆是一个运行时数据区, 堆是由垃圾回收处理的, 堆的优势可以动态的分配内存大小, 生存期不必事前告诉编译器, 因为它是运行时动态分配内存的, Java垃圾收集器会收走这些不再使用的内存数据, 但是也有缺点, 缺点是由于在运行时动态分配内存, 因此存取速度相对慢一些。
再来说一下栈Stack, 优势存取速度比堆要快, 仅此于计算机里的寄存器, 缺点是存在栈中的数据大小与生存期是己经确定的, 缺乏一些灵活性, 栈中主要是存储一些基础数据类型的变量, 比如int、short、long、byte、float、double、boolean、char和对象句柄。
Java内存模型它要求调用栈和本地变量存放在线程栈(ThreadStack)上, 对象存放在堆上(Heap), 具体说一下, 一个本地变量也可以指向的是一个对象的引用, 这种情况下引用的这个本地变量是存放在线程栈上, 但是对象本身是存放到堆上的, 一个对象它可能包含方法(methodOne\methodTwo), 这些方法可能包含本地变量(Localvariable1\LocalVariable2), 这些本地变量仍然是存放在线程栈上的, 即使这些方法所属的对象是存放在堆上的, 一个对象的成员变量会随着对象自身存放在堆上, 不管这个成员变量是原始类型和引用类型, 静态成员变量跟随类的定义一起存放到堆上, 存放在堆上的对象可以被所持有对这个对象引用的线程访问, 当一个线程可以访问这个对象的时候, 它也可以访问这个对象的成员变量, 如果两个线程同时调用同一个对象上的同个方法时, 它们都将访问这个对象上的成员变量, 但是每个线程都拥有了这个成员变量的私有拷贝, 这个特别重要
。
再来回顾下计算机硬件架构相关知识
上图是计算机硬件架构的简单图示。
1、CPU
.
上图是一个多CPU, 一个现代计算机通常有2个或者多个CPU, 其中一些CPU还有多核, 从这一点我们可以看出有2个或者多个CPU的现代计算机上, 同时运行多个线程是非常有可能的, 而且一个CPU在某个时刻运行一个线程是肯定没有问题的, 这意味着如果你的Java程序是多线程的, 在你的Java程序中, 每个CPU上一个线程, 是可能同时并发执行的。
2、CPU寄存器
.
每个CPU都包含一系列的寄存器, 它们是CPU内存的基础, CPU在寄存器上执行操作的速度, 远大于在主存上执行的速度, 这是因这CPU访问寄存器的速度远大于主存。
3、高速缓存(Cache)
.
由于计算机的存储设备与处理器的运算速度之间有着几个数量级的差距, 所以现代计算机系统都不得不加入一层读写速度都尽可能接近处理器速度接近的高级缓存, 来作为内存和处理器之间的缓冲, 将运算需要使用到的数据复制到缓存中, 让运算能快速的运行, 当运算结束后, 再从缓存同步到内存中, 这样处理器都无需等待缓慢的内存读写了, CPU访问缓存层的速度快于访问主存的速度, 但通常还是比访问内部寄存器的速度还是要慢一点, 每个CPU可能有一个CPU的缓存层, 一个CPU还有多层缓存, 在某一时刻一个或者多个缓存行可能被读到缓存, 一个或者多个缓存行可能被刷新到主存, 同一时间点在这里面有很多操作.
4、内存
一个计算机还包括一个主存, 所有的CPU都可能访问主存, 主存通常比CPU中的缓存大得多。
简单说一下它们的动作原理
:
通常情况下当一个CPU要读到主存的时候, 它会将主存的部分读取到CPU缓存中, 它甚至可能将缓存中的内容读到内部寄存器里面, 然后在寄存器中执行操作, 当CPU需要将结果回写到主存的时候, 它会将内部寄存器的值刷新到缓存中, 然后在某个时间点将值刷新到主存.
接下来我们来看一下, Java内存模型和硬件架构的一些关联.
Java内存模型和硬件架构的一些关联
通过图我们可以看出来, Java内存模型与硬件内存架构之间是存在一些差异的, 硬件内存架构它没有区分线程栈和堆, 对于硬件而言所有的线程栈和堆都分布在主内存里面, 部分线程栈和堆可能有些时间会出现在CPU缓存中和寄存器中。
Java内存模型抽象结构
接下来我们从抽象的角度看一下, 线程和主内存之间的抽象关系, 线程之间共享变量存放在主内存里面, 每个线程都有一个私有的本地内存, 本地内存是Java内存模型的一个抽象概念, 它并不是真实存在的, 它涵盖了缓存、写缓存区、寄存器以及其他硬件与编译器的优化, 本地内存中它存储了该线程以读或写共享变量拷贝的一个副本。
比如这里线程A要使用主内存的一个变量, 那么它先拷贝出一个变量的副本先放在本地内存里面, 从更低层次来说, 主内存就是硬件的一个内存, 是为了获取更好的运行速度虚拟机和硬件系统可能会让工作内存优先存储于寄存器和高速缓存中, Java内存模型中的线程的工作内存是CPU的寄存器和高速缓存的一个抽象的描述, 而JVM的静态内存存储模型(就是我们说的jvm内存模型)它只是对内存的物理划分而已, 它只局限在内存, 而且只局限在JVM的内存, 现在如果线程间通信它要求必须经过主内存, 如果线程A与线程B要通信的话, 必须要经历下面两个步骤.
线程A要把本地内存A中更新过的共享变量刷新到主内存里面去, 线程B要到主内存中去读取线程A之前更新过的共享变量。
接下来我们根据这个图来推演一下, 线程A和线程B同时对共享变量a做加1操作, 出现的线程安全问题。
伪代码如下:
private int a = 1;
public void add() {
a++;
}
线程A和线程B在执行过程中两个线程的数据是不可见的, 因此计数就出现了错误, 这个时候我们就必须增加一些同步的手段来保证并发时程序处理的准确性。
接下来, 讲一下Java内存模型定义的8种同步操作和定义的规则。
Java内存模型–同步操作与规则
一、Java内存模型–同步八种操作
1、lock(锁定): 作用于主内存的变量, 把一个变量标识为一条线程独占状态
2、unlock(解锁): 作用于主内存的变量, 把一个处于锁定状态的变量释放出来, 释放后的变量才可以被其它线程锁定
3、read(读取): 作用于主内存的变量, 把一个变量值从主内存传转到线程的工作内存中, 以便随后的load动作使用
4、load(载入): 作用于工作内存的变量, 它把read操作从主内存中得到的变量值放入工作
5、use(使用): 作用于工作内存的变量, 把工作内存中的一个变量值传递给执行引擎
6、assign(赋值): 作用于工作内存的变量, 它把一个从执行引擎接收到的值赋值给工作内存的变量
7、store(存储): 作用于工作内存的变量, 把工作内存中的一个变量的值传送到内存中, 以便随后的write的操作
8、write(写入): 作用于主内存的变量, 它把store操作从工作内存中一个变量的值传送到主内存的变量中
Java内存模型–同步规则
1、如果要把一个变量从主内存中复制到工作内存, 就需要按顺序地执行read和load操作, 如果把变量从工作内存中同步回主内存中, 就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行, 而没胡保证必须连续执行
2、不允许read和load、store和write操作之一单独出现
3、不允许一个线程丢弃它的最近assign的操作, 即变量在工作内存中改变了之后必须同步到主内存中
4、不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中
5、一个新的变量只能在主内存中诞生, 不允许在工作内存中直接使用一个未被初始化(load和assign)的变量.即就是对一个变量实施use和store操作之前, 必须先执行过了assign和load操作
6、一个变量在同一时刻只允许一条线程对其进行lock操作, 但lock操作可以被同一条线程重复执行多次, 多次执行lock后, 只有执行相同次数的unlock操作, 变量才会被解锁. lock和unlock必须成对出现
7、如果对一个变量执行lock操作, 将会清空工作内存此变量的值, 在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值。
8、如果一个变量事先没有被lock操作锁定, 则不允许对它执行unlock操作; 也不允许去unlock一个被其他线程锁定的变量
9、对一个变量执行unlock操作之前, 必须先把此变量同步到主内存中(执行store和write操作)
基本规则就讲完了, 这些同步操作和对应的基本规则是Java里并发相关的类在设计时都必须遵守的。
至此Java内存模型相关的知识我们都介绍完了, 我们再简单回顾一下Java内存模型。
首先Java内存模型是一种规范, 它规定了一个线程如何和何时可以看到由其它线程修改过后的共享变量的值, 以及在必须时如何同步的访问共享变量, 它要求调用栈和本地变量存放在线程栈上, 对象存放在堆上, 线程间的通信必须要经过主内存, 同时它定义了同步的八个操作及基本规则, 这个为我们处理并发问题提供了理论基础。
关于为什么存在线程安全? 我相信上面的文字己经说明清楚了, 关于JVM、JDK提供了哪些组件来帮助我们更好的编写线程安全的程序, 我下面提供了一张知识结构图, 肯定不全, 但是常用的肯定在里面.