内存模型
特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象
Java虚拟机规范中试图定义一种Java内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下达到一致的内存访问效果
Java内存模型的主要目标是定义程序中各个变量(包括实例字段,静态字段和构成数组的对象元素,不包括局部变量与方法参数)的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节
Java内存模型规定所有的变量都存储在主内存中,每条线程中还有自己的工作内存,线程的工作内存中保存了该线程使用的变量的主内存的副本拷贝,线程对变量的所有操作(读取,赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量,不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要通过主内存来完成
内存之间的交互
Java内存模型通过定义8种操作来完成主内存和工作内存之间具体的交互,虚拟机保证了这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操作之前,必须将次变量同步到主内存中(执行store,write操作)
volatile 型变量的特殊规则
volatile是Java虚拟机提供的最轻量级的同步机制,Java内存模型对volatile进行特殊的访问规则
保证了变量所有线程的可见性,当一条线程修改了这个变量的值,新值对于其他线程来说立即可得知的,需要注意的是volatile变量在各个线程的工作内存可能出现不一致的情况,但由于使用之前需要先刷新,所有执行引擎看不到不一致的情况,但Java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的
禁止指令重排序优化,普通变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方能获取正确的结果,而不能保证变量赋值操作的顺序与程序代码执行顺序一致,通过添加内存屏障,使得本地CPU的Cache写入内存,并引起别的CPU或者其他内存无效化其Cache,从而保证volatile变量的修改对其他CPU立即可见
如何理解禁止指令重排序:指令重排序是指CPU采用了允许将多条指令不按程序规定顺序分开发送到各相应电路单元处理,但并不是说指令任意重排,CPU需要正确处理指令依赖情况保证程序能得出正确的执行结果,因此在本CPU执行,重排序看起来仍然有序,因此,通过添加内存屏障,保证所有之前的操作都已经执行完成,这样便形成了“指令重排序无法越过内存屏障的效果”
Java内存模型对volatile变量定义的特殊规则
- 线程T对变量V的use和load操作必须连续一起出现,要求在工作内存中,每次使用V之前都必须先从主内存刷新到最新的值,用来保证能看到其他线程对变量V所做的修改
- 线程T对变量V的assign和store操作必须连续一起出现,要求在工作内存中,每次修改V后必须立刻同步到主内存中,用于保证其他线程可以看到自己对变量V的修改
- 保证volatile修饰的变量不会被指令重排序,保证代码的执行顺序与程序的顺序相同
long和double 型变量特殊规则
对于64位的数据类型(long,double),在模型中特别定义了一条宽松的规定,允许虚拟机将没有别volatile修饰的64位数据类型的load,store,read和write这4个操作的原子性分为两次操作来进行,需要强调是,目前平台下虚拟机基本将64位的数据读写操作作为原子操作对待
原子性,可见性与有序性
- 原子性 :由Java内存模型直接保证的原子性操作包括read,load,assign,use,store和write,我们可以认为基本数据的访问读写是具备原子性的,如果场景需要一个更大范围的原子性保证,可以通过lock和unlock操作来满足需求,映射到Java代码中的——Synchronized关键字
- 可见性:当一个线程修改了共享变量的值,其他线程能够立即得知这个修改,Java内存模型通过变量修改后将新值同步回主内存,装载变量读取前从主内存刷新变量值 这种依赖主存作为传递媒介的方式实现可见性,除了volatile之外,synchronized和final 关键字也能实现可见性,同步块的可见性是指:对一个变量执行unlock操作之前,必须先把变量同步回主内存中 获得,final关键字可见性:被final修饰的字段在构造器一旦初始化完成,并构造器没有将this传递出去,在其他线程中就能看到final字段的值
- 有序性:java语言天然的有序性表现:在本线程观察,所有操作都是有序的,如果通过其他线程观察另外一个线程,所有操作都是无序的 前半句指“线程内表现为串行语义” 后半句表现为“指令重排序”现象和“工作内存与主内存同步延迟”现象,Java中volatile和synchronized保证线程的有序性,volatile本身就包含禁止指令重排序指令,而synchronized由“一个变量在同一时刻只允许一条线程对其进行lock操作”保证有序
先行发生原则(happen-before)
先行发生是Java内存模型中定义两项操作之间的偏序关系,如果操作A先行发生于操作B,操作A产生的影响能够被操作B观察到,“影响”包括修改了内存中共享变量的值,发送了消息,调用了方法等。无需通过同步手段能保证下列的先行发生规则
- 程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作,
- 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作
- volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作
- 线程启动规则:Thread对象start() 方法先于发生次线程的每一个动作
- 线程终止规则:线程中所有的操作先于此线程的终止检测,通过Thread.join()方法结束,Thread.isAlive()返回值检测线程已经终止执行
- 线程中断规则:对线程interrupt()方法调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Therad.interrupted()检测是否又中断发生
- 对象终结规则:一个对象初始化完成先于它的finalize()方法的开始
- 传递性:如果A先于B,B先于C,则可到操作A先于操作C
Note 一个操作“时间上的先发生”不代表这个操作是“先行发生”,时间先后顺序与先行发生原则基本没有太大关系,衡量一个并发安全问题不受时间顺序的干扰,一切必须以先行原则为准
Java与线程
线程是进程更轻量级别的调度执行单位,线程的引入,可以将一个进程的资源分配和执行调度分开,各个线程共享进程的资源(内存地址,文件I/O等),又可以独立调度
主流的操作系统都提供了线程实现,Java语言提供了在不同硬件和操作系统平台下对线程的统一处理,每个已经执行start()且还未结束的Thread类的实例就代表了一个线程,需要注意的是,Thread中大部分API都是Native的,主要通过 使用内核线程实现,使用用户线程实现和使用用户线程加轻量级进程混合实现
使用内核实现
内核线程就是直接有操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操作调度器对线程进行调度,并负责将线程的任务映射到各个处理器上,每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就叫多线程内核
程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程,就是通常意义的线程,由于每个轻量级 别进程都需要一个内核线程支持,因此只有线支持内核线程,才能支持轻量级进程,通常是1:1的映射关系
由于内核线程的支持,每个轻量级进程成为一个独立的调度单元,即使有一个轻量级进程在系统调用阻塞了,也不是影响这个进程继续工作,但也存在局限性:所有的线程操作,如创建,析构,同步,都需要进行系统调用,而系统调用代价比较高,其次每个轻量级进程都需要一个内核线程支持,因此要消耗一定的内核资源,支持的轻量级进程数量有限
使用用户线程实现
建立在用户空间的线程库上,系统内核不能感知线程存在的实现,用户线程的建立,同步,销毁,和调度完全在用户态中完成,不需要内核的帮助,因此操作可以快速且低消耗,这种进程和用户线程之间1:N的一对多的关系
使用用户线程的优势在于不需要系统内核,缺点也是因为没有系统内核的支持,所有的线程操作都需要用户自己处理,实现复杂,现在一般都放弃使用
使用用户线程加轻量级进程混合使用
混合使用,既存在用户线程,又存在轻量级进程,用户线程还是完全建立在用户空间,而操作系统提供的轻量级进程则作为用户线程和内核线程之间的桥梁,对应的N:M的关系
Java线程调度
线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种:协同式线程调度和抢占式线程调度
协同式调度
线程的执行时间由线程本身控制,线程把自己的工作完成后,主动通过系统切换到另外一个线程上,好处是实现简单,没有什么同步问题,同时也存在线程执行不可控的风险
抢占式调度
每个线程将有系统分配执行,线程的切换不由线程自身决定,是系统可控制的,
状态转换
Java语言定义了5种线程状态,在任何时候,一个线程有且只能其中一个状态
- 新建(new) 创建后尚未启动的线程
- 运行(Runable) 包括了操作系统中Running和Ready状态,此状态下的线程可能执行,也可能等待CPU分配执行时间
- 无限期等待(Waiting) 处于此状态的线程不会被分配CPU执行时间,它们等待被其他线程显示唤醒,可以通过Object.wait(),Thread.join(),LockSupport.park()让线程进入无限期的等待
- 限期等待(Timed Waiting):处于这种状态的线程也不会分配CUP执行时间,不过无需等待其他线程显示唤醒,在一定时间后它们会自动由系统唤醒,通过Thread.sleep(), Object.wait(xxx),Thread.join(xxx),LockSupport.parkNanos(),LockSupport.parkUntil()方法
- 阻塞(Blocked) 线程阻塞和线程等待区别是:阻塞在等待获取一个排他锁,这个事件将在另外一个线程放弃这个锁发生;而等待则是等待一段时间
- 结束(Terminated) 已终止线程的状态,线程完成结束执行