主要内容:虚拟机如何实现多线程、多线程之间因为共享和竞争数据而致使的一系列问题及解决方案。
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)。