【个人笔记】
1.JMM
Java内存模型:虚拟机定义的一种方式来屏蔽各种意见和操作系统的内存访问差异,以实现让Java程序在各种平台都能达到一直的内存访问效果。
1.1 主内存和工作内存
- 主内存:JMM规定所有的变量都存储在主内存中,包括实例字段、静态字段等,但是不包括局部变量(因为线程私有)。
- 工作内存:每个线程有自己的工作内存,保存该线程使用到的变量的主内存拷贝副本,线程对变量的所有操作都必须在工作内存中,而不能直接读写主内存中的变量。不同线程也不能直接访问其它线程的工作内存。
注意:这里的主内存和工作内存的概念和堆、栈、方法区 不是同一个层次的划分。
1.2 内存间的交互操作
每一个操作都是原子的、不可分的。对于long和double的load、write、read和store在某些平台例外。
- lock:作用于主内存;把一个变量标识为线程独占。
- unlock:作用于主内存;释放一个lock的变量,释放完才可以被别的线程lock。
- read:作用于主内存;将变量从主内存传到工作内存,以便load。
- load:作用于工作内存;将read来的变量放入工作内存的变量副本。
- use:作用于工作内存;把工作内存中的一个变量传递给执行引擎。虚拟机遇到需要使用变量的字节码时执行此操作。
- assign:作用于工作内存;将来自执行引擎的变量赋值给工作内存的变量。
- store:作用于工作内存;把工作内存的变量传递给主内存中,以便write。
- write:作用与主内存;把store从工作内存得到的变量放入主内存的变量中。
一些规则:
- 不允许read和load、store和write操作之一单独出现
- 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
- 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用-- 一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
- 一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
- 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
- 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。
1.3 volatile
volatile是JVM中最轻量级的同步机制,有2种特性:
- 保证变量的可见性:对volatile变量的写操作可以立刻反应到所有线程中。
- 禁止指令重排序。插入内存屏障保证后面的指令不会重排序到屏障之前。
volatile语义并不能保证变量的原子性。对任意单个volatile变量的读/写具有原子性,但类似于i++、i–这种复合操作不具有原子性,因为自增运算包括读取i的值、i值增加1、重新赋值3步操作,并不具备原子性。
1.4 long和double的非原子协定
- JMM要求lock、unlock、read、load、assign、use、store、write这8个操作都必须具有原子性,但对于64为的数据类型(long和double,具有非原子协定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为2次32位操作进行。
- 如果多个线程共享一个没有声明为volatile的long或double变量,并且同时读取和修改,某些线程可能会读取到一个既非原值,也不是其他线程修改值的代表了“半个变量”的数值。不过这种情况十分罕见。因为非原子协议换句话说,同样允许long和double的读写操作实现为原子操作,并且目前绝大多数的虚拟机都是这样做的。
1.5 原子性、可见性、有序性
原子性:JMM保证的原子性变量操作包括read、load、assign、use、store、write,而long、double非原子协定导致的非原子性操作基本可以忽略。如果需要对更大范围的代码实行原子性操作,则需要JMM提供的lock、unlock、synchronized等来保证。
可见性:前面分析volatile语义时已经提到,可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。JMM在变量修改后将新值同步回主内存,依赖主内存作为媒介,在变量被线程读取前从内存刷新变量新值,保证变量的可见性。普通变量和volatile变量都是如此,只不过volatile的特殊规则保证了这种可见性是立即得知的,而普通变量并不具备这种严格的可见性。除了volatile外,synchronized和final也能保证可见性。
有序性:JMM的有序性表现为:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句指“线程内表现为串行的语义”(as-if-serial),后半句值“指令重排序”和普通变量的”工作内存与主内存同步延迟“的现象。
1.6 先行发生原则
Java规定的一种规则,用于判断是否线程安全。满足的这些条件的无须任何线程同步手段。
- 程序次序规则(Program Order Rule):在同一个线程中,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操纵。准确的说是程序的控制流顺序,考虑分支和循环等。
- 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面(时间上的顺序)对同一个锁的lock操作。
- volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面(时间上的顺序)对该变量的读操作。
- 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。
- 线程终止规则(Thread Termination Rule):线程的所有操作都先行发生于对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
- 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断时事件的发生。Thread.interrupted()可以检测是否有中断发生。
- 对象终结规则(Finilizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()的开始。
- 传递性(Transitivity):如果操作A 先行发生于操作B,操作B 先行发生于操作C,那么可以得出A 先行发生于操作C。
例如:
//假设线程A在时间上先调用setValue(1),然后线程B调用getValue()方法,那么线程B收到的返回值一定是1吗?
private int value = 0;
public void setValue(int value) {
this.value = value;
}
public int getValue() {
return this.value;
}
//按照happens-before原则,两个操作不在同一个线程、没有通道锁同步、线程的相关启动、终止和中断以及对象终结和传递性等规则都与此处没有关系,因此这两个操作是不符合happens-before原则的,这里的并发操作是不安全的,返回值并不一定是1。
2. 线程
2.1 线程的实现(不特指Java线程)
使用内核线程实现
使用用户线程实现
使用用户线程加轻量级进程实现
内核线程:KLT,直接由操作系统内核支持的线程,由内核完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核叫做多线程内核。
程序一般不会去直接使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(LWP),即通常意义上的线程。由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。*轻量级进程与内核线程之间1:1关系称为一对一的线程模型。
优点:内核线程保证了每个轻量级进程都成为一个独立的调度单元,即时有一个轻量级进程在系统调用中阻塞了,也不会影响整个进程的继续工作。
缺点:基于内核线程实现,因此各线程操作等需要系统调用,系统调用代价高,需要在用户态和内核态来回切换,其次,每个轻量级进程都需要一个内核线程的支持,因此轻量级进程要消耗一定的内核资源,如内核线程的栈空间,因此一个系统支持轻量级进程的数量是有限的。
用户线程:用户线程指完全建立在用户空间的线程库上。这种线程不需要切换内核态,效率非常高且低消耗,也可以支持规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。
优缺点:用户线程优势在于不需要系统内核支援,劣势也在于没有系统内核的支援,所有的线程操作都是需要用户程序自己处理。阻塞处理等问题的解决十分困难,甚至不可能完成。所以使用用户线程会非常复杂。
用户线程加轻量级进程混合:这种混合下,既存在用户线程,也存在轻量级进程。用户线程还是完全建立在用户空间中,因此用户线程的创建,切换,析构等操作依然廉价,并且可以支持大规模的用户线程并发。而轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低整个进程被完全阻塞的风险。
2.2 线程调度
协同式:线程执行时间由线程本身控制,自己执行完了以后通知系统进行切换。线程执行时间不可控,容易GG。
抢占式:线程执行时间由系统来分配。Java是抢占式。