主要内容:虚拟机如何实现多线程、多线程之间由于共享和竞争数据而导致的一系列问题及解决方案。
Java内存模型:
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。(这里所说的变量是指可能存在竞争问题的实例字段、静态字段和构成数据对象的元素)
Java内存模型规定了所有的变量都存储在虚拟机内存的主内存(Main Memory)中,每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在自己的工作内存中进行,而不能直接读写主内存中的变量。
Java内存模型定义了8种操作来完成主内存和工作内存之间的交互:
作用于主内存:lock(锁定)、unlock(解锁)、read(读取)、write(写入)
作用于工作内存: load(载入)、use(使用)、assign(赋值)、store(存储)
规则:需要顺序执行read和load、store和write【不必保证连续执行】。
不允许read和load、store和write操作之一单独出现。
不允许线程丢弃它最近的assign操作,必须同步回主内存。
不允许线程无原因(没有发生过任何assign操作)的把数据同步回主内存。
新变量只能在主内存中诞生,不允许在工作内存中直接使用一个未初始化(load或assign)的变量。
一个变量同一时刻只允许一条线程对其进行lock操作,可多次lock,需要该线程进行等量的unlock操作才会被解锁。
如果对一个变量执行lock操作,将会清除工作内存中此变量的值,在使用此变量前需要重新初始化到工作内存中。
unlock操作必须在lock操作之后,且不可跨线程。
对一个变量unlock操作前,必须先把变量同步回主内存中。
volatile型变量的特殊规则:
关键字volatile是虚拟机提供的最轻量级的同步机制。
volatile变量具备的两种特性:a)变量对所以线程的可见性。b)禁止指令重排序优化。
volatile变量在大多数场景下,总开销仍然要比锁(使用synchronized关键字或java.util.concurrent包里面的锁)来的低。
对于long和double类型的非原子性操作规则:
Java内存模型定义的8种操作都具有原子性,但是对64位的数据(long和double)特别定义了一条规则(非原子性协定):允许未被volatile修饰的64位的数据读写操作划分为两次32位的操作来进行。
原子性、可见性和有序性:
Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这三个特征来建立的。
原子性(Automicity):由Java内存模型来保证的的原子性变量操作包括read(读取)、write(写入)、load(载入)、use(使用)、assign(赋值)、store(存储)这六个。更大范围的原子性保证需要使用到synchronized关键字,即synchronized块之间的操作也具备原子性。
可见性(Visibility):当一个线程修改了共享变量的值,其他线程立即得到这个修改。Java内存模型是通过在变量修改后将变量同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的。可以看做是将read->load->use、assign->store->write组合成两个原子性操作实现可见性。
有序性(Ordering):如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。无序性主要由于存在”指令重排序“和”工作内存与主内存同步延迟“。
volatile能保证可见性,却不保证有序性,而synchronized能保证三种特性,但是对性能有影响。(PS:volatile是否保证有序性有一定的争议,参考其他文章后认为其不能保证)
先行发生原则:
程序次序规则(Program Order Rule)、管程锁定规则(Monitor Lock Rule)、volatile变量规则(Volatile Variable Rule)、线程启动规则(Thread Start Rule)、线程终止规则(Thread Termination Rule)、线程中断规则(Thread Interruption Rule)、对象终结规则(Finalizer Rule)、传递性(Transitivity)
先行发生原则是判断数据是否存在竞争,线程是否安全的主要依据。
先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,也就是说发生操作B之前,操作A产生的影响能被操作B观察到。
时间上的先后顺序和先行发生原则之间基本没有太大关系。
线程的实现:
线程是比进程更轻量级的调度执行单位,各个线程可以共享进程的资源,又可以独立调度(线程是CPU调度的最基本单位)。
实现线程主要有三种方式:使用内核线程实现,使用用户线程实现,使用用户线程加轻量级进程混合实现。
使用内核线程实现:
直接由操作系统内核(多线程内核)支持的线程,线程切换由内核来完成,内核通操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。
程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程——每个轻量级进程都有都由一个内核线程支持。
缺陷:系统调用的代价相对较高(需要在用户态和内核态中来回切换)、消耗内核资源(如内核线程栈空间)=>一个系统支持轻量级进程的数量是有限的。
使用用户线程实现:
完全建立在用户空间的线程库上,系统内核不能感知到线程存在的实现。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。
基本不需要切换到内核态,因此操作可以是非常快速且低消耗的。
没有系统内核的支援,所有的线程操作都需要用户程序自己处理,比较复杂。
使用用户线程加轻量级进程混合实现:
既存在用户线程,也存在请练级进程。用户线程可以支持大规模的用户线程并发,而操作系统提供支持的轻量级进程则作为用户线程和内核线程之间的桥梁。
混合模式下,用户线程和轻量级进程的数量比是不定的。
Java线程的实现:
jdk1.2之前使用的是用户线程,之后的版本线程模型与平台相关。
主流的操作系统都提供了线程实现,Java语言则提供了在不同硬件和操作系统平台下对线程的统一处理,每个java.lang.Thread类的实例就代表了一个线程。
Thread类的所有关键方法都被声明为Native,意味着这些方法没有使用或无法使用平台无关的手段来实现,或者为了执行效率而使用Native方法。
对于Sun JDK来说,它的windows版和linux版都是使用一对一的线程模型来实现的,一条Java线程映射到一条轻量级进程之中。
Java线程调度:
线程调度是指系统为线程分配处理器使用权的过程,主要有两种调度方式:协同式线程调度(Cooperation Threads-Scheduling)、抢占式线程调度(Preemptive Threads-Scheduling)。
协同式线程调度:
线程执行时间由线程本身来控制,线程执行完成后,要主动通知系统切换到另一个线程上去。
实现简单,由于线程切换操作对线程自己是可知的,所以没有什么线程同步的问题。
线程执行时间不可控制,如果一直不通知系统进行线程切换,可能导致整个程序阻塞。
抢占式线程调度(目前Java使用的调度方式):
每个线程将由系统分配执行时间,线程的切换不由线程本身来决定(Java中线程可以提前让出,但不可以强制获取到执行时间)。
线程的执行时间是系统可控的,不会出现一个线程导致整个进程阻塞的问题。
通过设置线程的优先级,可以建议系统给线程分配的时间不同。线程优先级高的容易被系统选择执行,但不保证一定优先选择执行。
Java线程状态转换:
Java语言定义了5种线程状态,在任意一个时间点,一个线程只能有且只有其中的一种状态:新建(New)、运行(Runable)、无限期等待(Waiting)、限期等待(Timed Waiting)、阻塞(Blocked)、结束(Terminated)。