十一、JVM(HotSpot)Java内存模型与线程

注:本博文主要是基于JDK1.7会适当加入1.8内容。

1、Java内存模型

内存模型:在特定的操作协议下,对特定的内存或高速缓存进行读写访问的抽象过程。不同的物理机拥有不一样的内存模型,而Java虚拟机也拥有自己的内存模型。
主要目标:定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。这里的变量与Java编程中所说的变量存在区别,它包括了实例变量、静态字段和构成数组对象的元素,但不包括局部变量和方法参数,因为后者都是线程私有不存在共享也就不存在竞争问题。

(1)主内存和工作内存

Java内存模型规定了所有的变量存储下主内存中,每个线程有自己的工作内存,工作内存中保存了该线程使用到的主内存副本拷贝,线程对变量的所有操作需要在工作内存中进行,而不能直接读写主内存中的变量。Java工作内存和主内存

(2)内存间交互操作

  • lock,锁定:作用于主内存的变量,把一个变量标识为一条线程独占的状态;
  • unlock,解锁:作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
  • read,读取:作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中,方便随后的load动作使用;
  • load,载入:作用于工作内存的变量,把read操作从主内存中得到的变量值放到工作内存的变量副本中;
  • use,使用:作用域工作内存的变量,把工作内存中的一个变量的值传给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时会执行这个操作;
  • assign,赋值:作用于工作内存的变量,把一个从执行引擎收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作;
  • store,存储:作用于工作内存的变量,把工作内存中一个变量的值传送给主内存中,方便随后的write操作使用;
  • write,写入:作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存变量中。

从工作内存同步到主内存需要顺序执行store和write操作,从主内存复制到工作内存需要顺序执行read和load操作。Java内存模型规定在执行上必须满足一下规则:

  • 不允许read和load、store和write操作单一出现,即不允许一个变量从主内存读取了但工作内存不接收,或者工作内存发起回写了但主内存不接收;
  • 不允许一个线程丢弃最近的assign操作,即变量在工作内存中改变了之后必须要把变化同步到主内存中;
  • 不允许一个线程无原因的把数组从线程的工作内存同步回主内存(性能考量);
  • 一个新变量只能在主内存中诞生,不允许在工作内存中直接使用一个未初始化的变量;
  • 一个变量同一时刻只允许一条线程对其lock操作,但lock操作可以被同一条线程执行多次,多次执行lock后需要执行相同次数的unlock操作,变量才会解锁;
  • 如果对一个变量进行lock操作,会清空工作内存中该变量的值,在执行引擎使用这个变量前需要重新执行load和assign操作;
  • 如果一个变量事先没有被lock锁定,那不允许unlock操作出现,也不允许unlock其他一个线程锁定的线程;
  • 对一个变量执行unlock操作之前,必须先把该变量同步回主内存中。

2、对于volatile型变量的特殊规则

  • 可见性
  • 禁止指令重排序

volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,仍然需要通过加锁保证原子性:(1)运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值;(2)变量不需要与其他的状态变量共同参与不变约束。

(1)原子性、可见性和有序性

原子性:由Java内存模型直接保证原子性变量操作包括read、load、assign、use、store和write。
可见性:volatile实现,final与synchronized也可以实现。
有序性:volatile实现,synchronized也可实现。

(2)先行发生原则

  • 程序次序规则:同一个线程中,按照程序代码顺序。
  • 管程锁定规则:一个unlock操作先行发生与后面对同一个锁的lock操作。
  • volatile变量规则:volatile变量写操作先行发生于后面这个变量的读操作。
  • 线程启动规则:Thread对象start方法先行发生于此线程的每个操作。
  • 线程终止规则:Thread对象所有操作先行发生于对此对象的终止检测,通过Thread.join()方法结束,Thread.isAlive()返回值检测。
  • 线程中断规则:对线程interrupt()方法调用先行发生于被中断的代码检测到中断事件的发生,通过Thread.interrupt()方法检测。
  • 对象终结规则:一个对象初始化完成先行发生于它的finalize()方法的开始。
  • 传递性:如果A先行发生于B,B先行发生于C,那么A先行发生于C。

时间先后顺序发生与先行性发生原则之间基本没有太大关系,衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准。

(3)线程的实现:线程是CPU调度的基本单位

实现线程的3种方式分别是:使用内核线程实现;使用用户线程实现;使用用户线程加轻量级进程混合实现。

内核线程

直接由操作系统内核支持的线程,由内核完成线程切换,内核通过操作调度器对线程进行调度,并负责线程的任务映射到各个处理器上。每个内核线程可视为内核的一个分身,这样操作系统就有能力同时处理多件时间,支持多线程的内核就叫做多线程内核。程序一般不会直接使用内核线程而是使用内核线程的一种高级接口——轻量级进程,也就是通常意义上所说的线程。轻量级进程具有它的局限性:首先,由于是基于系统内核线程实现,所以线程操作如创建、析构、同步等都需要进行系统调用,而系统调用代价相对较高,需要在用户态和内核态来回切换;其次,每个轻量级进程都需要一个内核线程支持,因此消耗一定的内核资源,内核资源是有限的,所以轻量级进程也是有限的。

用户线程

广义上一个线程主要不是内核线程就是用户线程,这样轻量级进程(基于内核线程的高级接口)也属于用户线程,但它始终建立在内核线程上,许多操作需要进行系统调用,效率受限。狭义上,用户线程是完全建立在用户控件的线程库上,系统内核不能感知线程存在的实现。如果实现得当,不需要切换到内核态,操作快速低耗,也可支持规模更大的线程数量。

用户线程和轻量级进程混合

用户线程完全建立在用户控件中,因此用户线程创建、切换、析构等操作廉价并支持大规模用户线程并发,操作系统提供支持轻量级进程作为用户线程和内核线程的桥梁,内核提供线程调度功能及处理映射,并且用户线程的系统调用需要通过轻量级进程来完成,大大降低整个进程被完全阻塞的风险。

Java线程

Windows和Linux版本都是一对一的线程模型实现,一条Java线程映射到一条轻量级进程中,因为Windows和Linux系统提供的线程模型是一对一的。Solaris平台由于操作系统线程特性可以同时支持一对一、多对多的线程模型。

(4)Java线程调度:系统为线程分配处理器使用权的过程,分别是协同式线程调度和抢占式线程调度

协同式线程调度:线程执行时间由线程本身控制,线程把自己工作完成后主动通知系统切换到另一个线程。优点:实现简单,没什么线程同步问题,Lua语言中的协程就是这类实现;缺点:线程执行时间不可控,如果线程出现问题会一直阻塞。
抢占式线程调度:每个线程将有系统分配执行时间,线程切换不由线程本身决定。优点:线程执行时间系统可控,不会出现一个线程导致整个进程阻塞的问题,Java使用的线程调度方式就是抢占式线程调度。如果想自定义线程执行时间或者优先级,Java定义了Thread.MIN_PRIORITY和Thread.MAX_PRIORITY,但是不能完全依赖,因为Java线程要映射到操作系统实现,不同的操作系统对线程优先级支持不一样,优先级级别定义也不一定一样。

(5)状态转换:新建–运行–等待(无限期和有限期)–阻塞–结束

猜你喜欢

转载自blog.csdn.net/zhangwei408089826/article/details/81869947