深入理解JAVA虚拟机 高效并发
处理器和缓存
由于计算机的存储设备与处理器的运算速度之间有着几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存来作为内存与处理之间的缓冲:将运算需要使用的数据复制到缓存中,让运算速度快速运行,当运算结束后再从弄个缓存同步回内存之中,这样处理器就无需等待缓慢的内存读写了。
基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也引入了新的问题:缓存一致性。为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作。
除此之外,为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行重排序。
JAVA内存模型:
处理器有自身的内存和缓存,重排序等功能,JVM定义了在这些硬件之上的通用框架,所以java试图通过定义一个"内存模型"来规范JVM的多CPU访问下的内存处理逻辑(其实就是模拟上面的图构建独立于硬件的JVM层面的类似内存和缓存以及一致性协议等机制)。为了让并发操作不产生歧义,这个内存模型需要足够严谨,同时,为了虚拟机的实现由足够的空间去利用硬件和各种特性来获得更好执行速度,内存模型又要足够宽松。
内存模型的主要目的是定义程序中各个变量的访问规则,即在虚拟机中将变量(静态字段,实例字段,不包含局部变量和方法参数)写入内存和从内存中读出到虚拟机的细节。
Java内存模型规定了所有的变量都存储在主内存(对应上面的内存)中。每条线程还有自己的工作内存(对应上面的高速缓存),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值得传递均需要通过主存来完成,线程、主内存、工作内存三者的交互关系如下图。
内存间交互操作
关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实验细节,Java内存模型中定义了8种操作来完成:
- lock(锁定):作为主内存的变量,它把一个变量标识为一条线程独占的状态。
- unlock(解锁):作为主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其它线程锁定。
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个命令。
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
- write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
Java内存模型还规定了在执行上述8种基本操作时必须满足如下规则:
- 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。
- 不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步会主内存中。
- 一个新的变量只能在主内存中"诞生",不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过assign和load操作。
- 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock之后,只有执行相同次数的unlock操作,变量才会被解锁。
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
- 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定住的变量。
- 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)。
对于volatile修饰的变量,在每次访问的时候都要执行read 和load,每次写入的时候都要执行store和write。
Java与线程
实现线程主要有三种方式:使用内核线程实现,使用用户线程实现,使用用户线程加轻量级进程混合实现。
内核线程(Kernel Thread,KLT)就是直接由操作系统内核(Kernel)支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程都可以看做是内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就叫多线程内核(Multi-Threads Kernel)。
程序一般不会直接使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),轻量级进程就是我们通常意义上所讲的线程,由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。
广义上将,一个线程只要不是内核线程,那就可以认为是用户线程(User Thread,UT)。 狭义上的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到线程存在的实现。
线程除了依赖内核线程实现和完全由用户程序自己实现之外,还有一种将内核线程与用户线程一起使用的实现方式。在这种混合实现下,既存在用户线程,也存在轻量级进程。
Java线程目前是使用的内核线程。历史上曾经使用过用户线程,现在已经抛弃了。
Java线程调度
线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种,分别是协同式(Cooperative Threads-Scheduling)线程调度和抢占式线程调度(Preemptive Threads-Scheduling)。
如果使用协同式调度的多线程系统,线程的执行时间由线程本身来控制,线程把自己的工作执行完毕后,要主动通知系统切换到另一个线程上去。
如果使用抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(在Java中,Thread.yield()可以让出执行时间,但要获取执行时间的话,线程本身是没有什么办法的)。
Java使用的是抢占式调度。
状态转换
Java语言定义了5种线程状态,在任意一个时间点,一个线程只能有且只有其中的一种状态,这5种状态分别是:新建,运行,无限期等待,限期等待,阻塞,结束。