Java虚拟机--内存模型与线程
高速缓存:处理器要与内存交互,如读取、存储运算结果,而计算机的存储设备和处理器的运算速度差异巨大,所以加入一层读写速度和处理器接近的高速缓存来作为内存和处理器之间的缓冲——将运算所需数据复制到缓存中,使得运算能快速进行;当运算结束后再将缓存同步回内存中,这样处理器无需等待缓慢的内存读写。
每个处理器都有自己的高速缓存,它们都共享同一个主内存,当多个处理器的运算任务都涉及同一块主内存区域时,将导致各自的缓存数据不一致,此时要同步数据到主内存以哪个处理器的缓存为主?这就是缓存一致性。为了解决这个问题,需要遵循一些缓存一致性协议。
Java内存模型
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储带内存以及从内存中取出变量这样的底层细节。这里的变量指的是非线程私有的实例字段、静态字段和构成数组对象的元素,不包括局部变量和方法参数,因为它们是线程私有、不被共享的。
Java内存模型中,所有的变量都存储在主内存中,每条线程还有自己的工作内存(与高速缓存类比),线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量;不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需通过主内存完成。
如果将Java内存模型和Java堆、栈比较,主内存对应Java堆中的对象实例部分,工作内存对应虚拟机栈中的部分。
内存交互
主内存和工作内存之间需要交互,Java内存模型中有8种原子操作:
- lock:作用于主内存变量,将其标识为线程独占。
- unlock:作用于主内存变量,将其从锁定状态释放,释放后才可被其他线程锁定。
- read:作用于主内存的变量,将一个变量从主内存中传输到工作内存中,以便随后的load动作使用。
- load:作用于工作内存中的变量,把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use:作用于工作内存的变量,把工作内存中的一个变量的值传递给执行引擎,当虚拟机需要使用到变量的值的字节码指令时会执行这个操作。
- assign:作用于工作内存的变量,把一个从执行引擎接受到的值赋给工作内存中的变量。当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store:作用于工作内存中的变量,把工作内存中的一个变量的值传送到主内存中,以便之后write操作使用。
- write:作用于主内存中的变量,把store操作从工作内存中得到的变量值放入主内存的变量中。
如果一个变量从主内存复制到工作内存,必须先执行read然后执行load操作(read和load之间允许插入其他操作,只要保证这个顺序即可);如果要把变量从工作内存同步回主内存中,需要先执行store操作然后执行write操作(store和write之间允许插入其他操作,只要保证这个顺序即可)。
volatile关键字
一个变量被定义为volatile后,具备两个特性:
可见性。当一条线程修改了这个变量的值,新值能被其他线程立刻观察到。这是说:volatile的作用是在本CPU对变量的修改直接写入主内存中,同时这个写操作使得其他CPU中对应比变量的缓存行无效,这样其他线程在读取这个变量时候必须从主内存中读取,因为读取到的是最新的,这就是上面说得能被立即“看到”。
而且volatile在其汇编代码中有一个lock操作,这个操作相当于一个内存屏障,保证了有序性,禁止了指令重排:具体来说在执行到volatile变量时,其之前的语句一定被执行过了且结果对后面是已知的,而其后面的语句一定还没执行到;在volatile变量之前的语句不能被重排后其之后,相反其后的语句也不能被重排到之前。
而对于普通变量,普通变量在线程间的传递必须通过主内存这个“桥梁”,即须线程A修改了变量值后,向主内存回写,线程B在等回写完成后再从主内从中读取,新变量这时才对线程B可见。
volatile指令只能保证可见性、有序性,不能保证原子性。比如多个线程执行i++
时。i++
等价于i = i + 1
这语句分为三步,首先读取i的值,作加1操作,将新值赋给i。有种情况是,即使i被volatile修饰,保证读取到的值是最新的正确的,假如i初始值为0,现在A、B线程都正确读取到后i的值为0,B现在加1直接写入主内存中,回到A,因为A读取过了,且读到的是0,这是A线程加1操作写入主内存中,i还是1,进行了两次自增,最后i的值却值增加了1。所以在多线程中即使使用了volatile也不能保证线程安全。
原子性、可见性、有序性
- 原子性。Java内存模型能直接保证原子性变量操作有:read、load、assign、use、store、write,Java的基本数据类型的访问读写大致是具备原子性的。而lock和unlock这样的原子操作是由Java中的同步块——synchronized块,被其包围的块也具有原子性。
- 可见性。Java内存模型通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量,这些操作都以来主内存这个“桥梁”。普通变量和volatile变量都是如此,不过volatile可保证新值能立即同步到主内存;使用前立即从主内存刷新,可以说volatile保证了多线程操作时变量的可见性。synchronized和final关键字也可实现可见性,前者:对一个变量执行unlock之前,必须先把次变量同步回主内存中(store、write);后者:final修饰的字段在构造器中一旦初始化完成且this引用没有被传递出去(this引用逃逸),其他线程中能看见final字段的值。
- 有序性。在本线程内观察,所有操作都是有序的,这表现为“线程内表现为串行的语义”;在一个线程中观察另一个线程,所有操作都是无序的,表现为“指令重排”和“工作内存与主内存的延迟”。volatile本身禁止了指令重排,而synchronized保证了一个变量在同一个时刻只允许一条线程对其进行lock操作。
先行发生原则
如果操作A先于操作B发生,其实就是说发生操作B之前,操作A产生的影响(修改了共享变量的值、发送了消息、调用了方法等)能被操作B观察到。Java内存模型自身具备以下先行发生关系,这些原则是指令重排不可违背的。
- 程序次序原则:一个线程内保证语义的串行性
- 锁规则:一个unlock操作先行发生于后面对同一个锁的lock操作
- volatile规则:对一个volatile变量的写操作先行发生于对这个变量的操作
- 线程启动规则:线程的start()方法先于次线程的任何动作
- 线程终止规则:线程的所有操作都先行发生于线程终结
- 线程中断规则:对线程的interrupt()方法的调用先于被中断线程的代码
- 对象终结规则:对象的初始化完成先于它的finalize()方法
- 传递性:如果A先于B,B先于C,那么A先于C
线程
线程的实现主要有三种方式。
使用内核线程实现
内核线程就是直接由操作系统内核支持的线程,由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。
程序一般不直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程——也就是通常意义所讲的线程,每个轻量级进程都有一个内核线程支持,是一一对应的关系。
每个轻量级进程都成为一个独立的调度单元,即使有一个轻量级进程在系统调用中阻塞了,也不会影响整个进程继续工作。但是轻量级进程基于内核线程,需要进行系统调用,而系统调用的代价高,需要在用户态和内核态中来回切换,所以轻量级进程要消耗内核资源。
使用用户线程实现
狭义上的用户线程是无需内核帮助的,完全建立在用户空间的线程库上,进程与用户线程的关系是一对多的线程模型。
用户线程无需内核支援,但这也是个缺点,所有的操作都需要用户程序自己处理,所以使用用户线程实现的程序一般比较复杂。
混合实现
将轻量级进程和用户线程混合。这种模式下,既存在用户线程又存在轻量级进程,轻量级进程成为了用户线程和内核线程的桥梁。用户线程和轻量级进程的数量比不定,可以称为是多对多的模型。
Java线程调度
大体上有两种调度方式。
- 协同式线程调度:线程的执行时间由线程自己决定,当执行完自己的工作后,主动通知系统切换到另一个线程上。
- 抢占式线程调度:每个线程的执行时间是由系统分配的,线程的切换不由线程本身决定,不会出现一个线程导致整个进程阻塞的问题。
Java使用的线程调度方式就是抢占式线程调度。
Java的线程调度是系统自动完成的,但是可以通过设置线程的优先级,优先级越高的线程越容易被系统选择执行。
Java线程状态
共有6种线程状态。
- 新建(New):创建后尚未启动的线程;
- 运行(Runnable):包括了操作系统线程状态中的Running和Ready,此状态的线程有可能正在执行也可能正等待着CPU为它分配执行时间
- 无限期等待(Waiting):此状态下不会被分配CPU执行时间,需要等待被其他线程唤醒
- 限期等待():此状态下也不会被分配CPU时间,但无需被其他线程显式唤醒,在一定时间后会有系统自动唤醒
- 阻塞(Blocked):线程被阻塞,阻塞状态中的线程在等待着获取到一个排他锁,这个事件将在其他线程放弃这个锁时发生;和“等待状态”不同,等待是在等待一段时间或者其他的线程的显式唤醒,在程序进入同步区域时,线程会进入阻塞状态。
- 结束(Terminated):已终止线程的状,表示线程已经结束执行。
by @sunhaiyu
2018.6.20