八 Java内存模型与线程
1 Java内存模型
---主要目标:定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。
---此处的变量和Java中的变量有所区别,它包括类字段、实例字段和构成数组对象的元素,但不包括局部变量和方法参数。
---Java内存模型规定:
· 所有的变量都存储在主内存中;
· 每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中变量;
· 不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
---线程、主内存、工作内存三者的交互关系如下图:
2 内存间交互操作
---Java内存模型中定义了以下8种操作,并且每一种操作都是原子的、不可再分的(double、long存在例外):
· lock:作用于主内存的变量,把一个变量标识为线程独占的状态。
· unlock:作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才能被其他线程锁定。
· read:作用于主内存的变量,把一个变量的值从主内存中传输到线程的工作内存中,以便之后的load使用。
· load:作用于工作内存的变量,把read操作从主内存中得到的变量值放入工作内存的变量副本中。
· use:作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎。
· assign:作用于工作内存的变量,把一个从执行引擎接收到的值赋给工作内存的变量。
· store:作用于工作内存的变量,把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
· write:作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中。
---Java内存模型还规定了在执行上述8种操作时必须满足如下规则:
· 不允许read和load、store和write操作之一单独出现。
· 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
· 不允许一个线程无原因地(没有发生过任何地assign操作)把数据从线程的工作内存同步回主内存中。
· 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化的变量。
· 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复多次执行,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
· 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
· 如果一个变量事先没有被lock锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
· 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中。
3 volatile变量
---是Java虚拟机提供的最轻量级的同步机制;
---volatile变量的特性:
· 保证此变量对所有线程的可见性。即当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的;
备注:可见性说明volatile变量在各个线程的工作内存中不存在一致性问题,但是由于Java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的。
· 禁止指令重排序优化。指令重排序是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理,volatile关键字保证重排序时不能把后面的指令重排序到内存屏障之前的位置。
---使用volatile必须满足:
· 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值;
· 变量不需要与其他的状态变量共同参与不变约束。
---性能:
· 性能要优于锁(使用synchronized关键字或java.util.concurrent包里面的锁);
· volatile变量读操作的性能和普通变量几乎没有差别,写操作可能会慢一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
Java内存模型对volatile变量定义的特殊规则:
· 要求在工作内存中,每次使用volatile变量前都必须先从主内存刷新最新的值,用于保证能看见其他线程对volatile变量所做的修改后的值;
· 要求在工作内存中,每次修改volatile变量后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对volatile变量所作的修改;
· 要求volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序和程序顺序相同。
4 Java内存模型对long和double型变量的特殊规则
---long和double的非原子性协定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行。
5 原子性、可见性、有序性
(1)原子性
---8种操作:read、load、assign、use、store、write、lock、unlock。
(2)可见性
---Java语言中,volatile、synchronized、final关键字能实现可见性,如下:
· volatile关键字:略。
· synchronized关键字:由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中”这条规则获得。
· final关键字:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”引用传递出去,那在其他线程中就能看见final字段的值。
(3)有序性
---如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。
---Java语言提供了volatile和synchronized关键字来保证线程之间操作的有序性,如下:
· volatile关键字:本身包含了禁止指令重排序的语义;
· synchronize关键字:由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得。
6 先行发生原则
---是判断数据是否存在竞争、线程是否安全的的主要依据。
---Java内存模型下一些“天然的”先行发生关系:
· 程序次序规则:在一个线程内,按照程序控制流顺序,控制流前面的操作先行发生于控制流后面的操作。
· 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。
· volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作。
· 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
· 线程终止规则:线程中的所有操作先行发生于对此线程的终止检测。
· 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
· 对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始.
· 传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C。
---先行发生原则和时间先后顺序之间基本没有太大的关系。
7 线程的实现
(1)使用内核线程实现
---内核线程(KLT):由操作系统内核支持的线程。由内核来完成线程切换,内核通过操纵调度器对线程进行调度。
---轻量级进程(LWP):内核线程的一种高级接口。每个轻量级进程都由一个内核线程支持,轻量级进程与内核线程是1:1的关系。
---轻量级进程优点:每个轻量级进程都是一个独立的调度单元,即使有一个轻量级进程在系统调用中阻塞了,也不会影响整个进程继续工作。
---轻量级进程缺点:
· 各种线程操作如创建、析构和同步,都需要进行系统调用,而系统调用需要在用户态和内核态之间来回切换,代价相对较高;
· 需要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持轻量级进程的数量是有限的。
(2)使用用户线程实现
---用户线程:完全建立在用户空间的线程库上,系统内核不能感知线程存在的实现。
---用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。
---进程与用户线程之间是一对多的关系。
---使用用户线程实现的优点:
· 程序实现得当,不需要切换到内核态,操作是快速且低消耗的;
· 可以支持规模较大的线程数量。
---使用用户线程实现的缺点:所有的线程操作都需要用户程序自己处理,程序一般都比较复杂。
(3)使用用户线程加轻量级进程混合实现
---用户线程和轻量级进程的数量比是多对多的关系,即N : M。
---优点:
· 用户线程的创建、切换、析构等操作依然廉价;
· 支持大规模的用户线程并发;
· 使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用通过轻量级进程来完成,大大降低了整个进程被完全阻塞的风险。
(4) Java线程的实现
---对于Sun JDK来说,它的Windows版与Linux版都是使用一对一的线程模型实现的,一条Java线程就映射到一条轻量级进程之中。
8 Java线程调度
---线程调度:系统为线程分配处理器使用权的过程。
---主要调度方式:
(1)协同式线程调度
---概念:线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上。
---优点:实现简单·、不存在线程同步问题。
---缺点:线程执行时间不可控制,如果一个线程出现问题,一直不告诉系统进行系统切换,那么程序就会一直阻塞在那里。
(2)抢占式线程调度
---概念:每个线程由系统来分配执行时间,线程的切换不由线程本身来决定。
---优点:线程的执行时间可控,而且不会因为一个线程出现问题而导致整个进程阻塞。
---Java使用的线程调度方式就是抢占式调度。
---Java语言中,可以通过设置线程优先级的方式来调整各个线程的执行时间的多少。
---使用线程优先级并不大靠谱,原因是:
· Java的线程是通过映射到系统的原生线程上来实现的,线程调度最终还是取决于操作系统,而不同平台上的优先级分类数量不一致。
· 优先级还可能会被系统自行改变。
9 线程状态
---Java语言中定义了5中线程状态:
(1)新建:创建后尚未启动的线程处于这种状态。
(2)运行:处于此状态的线程可能正在执行,也可能正在等待CPU为它分配执行时间。
(3)无限期等待:处于这种状态的线程不会被CPU分配执行时间,它们需要被其它线程显式唤醒。
---三种让线程陷入无限期等待的方法:
· 没有设置Timeout参数的Object.wait()方法;
· 没有设置Timeout参数的Thread.join()方法;
· LockSupport.park()方法。
(4)限期等待:处于这种状态的线程不会被CPU分配执行时间,也不需要被其它线程显式唤醒,在一定时间后会由系统自动唤醒。
---五种会让线程进入限期等待的方法:
· Thread.sleep()方法;
· 设置了Timeout参数的Object.wait()方法;
· 设置了Timeout参数的Thread.join()方法;
· LockSupport.parkNanos()方法;
· LockSupport.partUntil()方法。
(5)阻塞:在程序等待进入同步区域的时候。
(6)结束:已终止线程的线程状态。
---转换关系如下图: