概述
计算机处理器的运算速度与它的存储和子通信系统的速度差距太大,导致处理器大部份时间都在空闲等待状态,如果不希望浪费处理器的资源,就要想办法压榨处理器。多任务处理被认为是非常有效的“压榨”手段。
一. 硬件效率与一致性
由于处理器和存储器之间的运算速度有着几个数量级的差距,所以现代计算机都不得不加上一层或多层读写速度竟可能接近处理器运算速度的高速缓存来作为处理器和内存之间的缓冲:将运算需要的数据从内存中复制到缓存中,让运算能够快速进行,当运算结束后再从缓存同步回内存中,这样处理器就不用等待缓慢的内存读写了。
在多处理器系统中,这种结构也引发了一个新的问题,缓存一致性!一台计算机中有多个处理器,每个处理器都有自己的高速缓存,而它们又共享同一个主内存。当多个处理器的运算任务都涉及到同一块主内存区域时,将可能导致各自的缓存数据不一致。所以需要制定对应的缓存一致性协议,来保证各处理器的缓存数据一致。
除了增加高速缓存之外,为了使得处理器内部的运算单元能尽量被利用,处理器可能会对输入的代码进行乱序执行优化,执行器会将乱序执行的结果进行重组,保证该结果与顺序执行的结果一致,但是其顺序性不一定和代码的先后顺序相同。
二. Java内存模型
用来屏蔽各种硬件和操作系统之间的差异性,以实现让Java程序在各种平台下都能够达到一致的内存访问效果。
Java内存模型规定了所有的变量都存储在主内存(不是硬件的内存,而是虚拟机内存一部分)中,除此外每个线程还有自己的工作内存(与处理器高速缓存类似)。线程的工作内存中保存了该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的数据,不同的线程之间也无法直接访问其他线程的变量,线程之间变量值的传递均需要通过主内存来完成,线程,主内存,工作内存三者关系如图。
三. 内存间的交互
关于如何将变量从主内存拷贝到工作内存,如何从工作内存同步回主内存这一类的实现细节,Java内存模型中定义了以下八种操作来完成。
lock: 作用于主内存变量,把一个变量标识为一条线程独占的状态。
unlock:作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read:作用于主内存的变量,它把一个变量的值从主内存中传输到线程的工作内存中,以便随后的load动作使用。
load:作用于工作内存的变量,把read操作从主内存中得到的变量值放入到工作内存的变量副本中
use:作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用给变量赋值的字节码指令时执行这个操作。
assign:作用于工作内存变量,把从执行引擎中接收的值赋值给工作内存变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store:作用于工作内存变量,把工作内存中变量的值传送到主内存中,以便随后的write操作使用。
write:作用于主内存中的变量,把store操作从工作内存中得到的变量值放入到主内存变量中。
复制代码
volatile变量的特殊规则
- 保证共享变量的可见性。
可见性指当一条线程修改了这个变量的值,新值对于其他线程来说时立即可见的。而普通变量并不能做到这一点,普通变量的值在线程间传递时均需要通过主内存来完成。
- 禁止指令重排序
加了volatile关键字的变量在编译成Java字节码的时候会在被修改的时候添加lock操作,相当于一个内存屏障
并发三大特性
原子性
在某一时间段同一时刻只能有一个线程执行,其他线程不能执行,也就是“加锁”后的代码块。
可见性
当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。
有序性
Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的,如果在一个线程中观察另一个线程,那么所有的操作都是无序的。前面介绍过处理器在保证运算结果正确的情况下出现的指令重排序的现象。
先行发生原则 [Happen Before]
- 程序次序规则
在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。
- 管程锁定规则
一个unlock操作先行发生于后面对同一个锁的lock操作。
- volatile变量规则
对一个volatile变量的写操作先行发生于后面对这个变量的读操作。
- 线程启动规则
Thread.start()方法先行发生于此线程的每一个动作。
- 线程终止规则
线程中的所有操作都先行发生于此线程的终止检测。
- 传递性
如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。
Java语言无须使用任何同步手段保障就能够成立的先行发生规则有且只有上面这些。
线程的实现方式
内核线程实现
内核线程是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操作调度器对线程进行调度,并负责将线程的任务映射到各个处理器上,每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情。
程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程,由于每一个轻量级 进程都由一个内核线程支持,因此只有先支持内核线程才能有轻量级进程。
用户线程实现
广义上来讲,一个线程只要不是内核线程,都可以认为是用户线程。狭义上用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到用户线程的存在及如何实现的。用户线程的建立,同步,销毁和调度完全在用户态中完成,不需要内核的帮助。
混合实现(Java线程实现方式)
线程除了依赖内核线程实现和完全由用户线程自己实现之外,还有一种将内核线程和用户线程一起使用的方式。这种混合实现的方式下,既有用户线程,又有内核线程。用户线程还是完全建立在用户空间中,因此用户线程的创建,切换等操作依然廉价,并且可以支持大规模的用户线程并发。而操作系统支持的轻量级进程则可以作为用户线程和内核线程之间的桥梁,降低整个进程被阻塞的风险。
内核线程的调度成本为什么高?
其成本主要来自用户态和内核态之间的状态转换,而这两种状态转换的开销主要来自于响应中断、保护和恢复执行现场的成本。
总结
- 处理器进行运算时不是直接和内存进行交互处理数据的,而是在处理器的高速缓存中进行运算处理,处理完成后再同步回内存。
- Java内存模型是一组抽象的概念,并不真实存在,它描述了程序中各个变量的访问方式。主内存就是对应着方法区和堆区,工作内存就是Java虚拟机栈和本地方法栈。
- volatile的两大作用:禁止指令重排序、保证共享变量的可见性
- 线程的实现方式后三种,Java线程使用的是内核线程+用户线程的方式来共同实现的。
- Java程序中的先行发生原则。
- 用户态转换到内核态为什么影响性能。