Java内存模型与线程实现——深入理解Java虚拟机

来源:互联网 发布:淘宝卖家信用借贷 编辑:程序博客网 时间:2024/05/22 08:01

Java内存模型与线程

一、硬件的效率与一致性

这里写图片描述

 由于计算机的存储设备与处理器的运算能力之间有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中没这样处理器就无需等待缓慢的内存读写了。
  基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是引入了一个新的问题:缓存一致性(Cache Coherence)。在多处理器系统中,每个处理器都有自己的高速缓存,而他们又共享同一主存,如图所示:多个处理器运算任务都涉及同一块主存,需要一种协议可以保障数据的一致性,这类协议有MSI、MESI、MOSI及Dragon Protocol等。Java虚拟机内存模型中定义的内存访问操作与硬件的缓存访问操作是具有可比性的,后续将介绍Java内存模型。

二、Java内存模型

一、主内存与工作内存

Java内存模型的主要目标定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存取出变量这样的底层细节。这里的变量指的是实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享。

Java内存模型规定所有的变量存储在主内存(虚拟机内存的一部分)中,每条线程有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、幅值)都必须在工作内存进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存的变量,线程间变量值的传递需要通过主内存完成。

这里写图片描述

主内存主要对应于Java堆的对象实例数据部分(内存),工作内存对应于虚拟机栈中的部分区域。(高速缓存)

二、内存间交互操作(原子操作)

关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:

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

Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。也就是read和load之间,store和write之间是可以插入其他指令的,如对主内存中的变量a、b进行访问时,可能的顺序是read a,read b,load b, load a

8种操作需要满足以下规则

  • 不允许read和load、store和write操作之一单独出现
  • 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
  • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

三、volatile变量的特殊规则

Java虚拟机提供的最轻量级的同步机制,具备两种特性。

1.变量对所有线程的可见性保证

指当一条线程修改了变量的值,新值对于其他线程来说是可以立即得知的。volatile变量在各个线程的工作内存中不存在一致性问题,但Java里面的运算并非原子操作(执行引擎执行字节码操作并非原子操作),导致volatile变量的运算在并发下一样是不安全的。

    public static void increase(){        race ++;  //字节码并非原子操作    }

由于volatile变量只能保证可见性,在不符合以下两条规则的情况下,仍然需要加锁来保证原子性。

  • 运算结果并不依赖于变量的当前值,或者能够确保只有单一的线程修改变量的值。
  • 变量不需要与其他的状态变量共同参与不变约束。

2.禁止指令重排序优化

volatile变量操作时对应的汇编指令会生成内存屏障(lock)重排序时不能把内存屏障后面的指令重排序到内存屏障之前的位置,只有一个CPU时,不需要内存屏障;但如果有很多CPU访问同一个内存,且其中一个在观测另一个(状态),就需要内存屏障来保证一致性。(保证不会处理器乱序执行)

3.Java内存模型对volatile变量的特殊规则:

  • 在工作内存中,每次使用变量V前都必须先从主内存刷新(读取)最新的值,用于保证能看见其他线程对变量V所作修改后的值。
  • 在工作内存中,每次修改变量V后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量V所做的修改。
  • 保证代码执行顺序与程序顺序一致

4.对于long和double类型变量的特殊规则

非原子协定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即虚拟机实现选择可以不保证64为数据类型的load、store、read、write这四个操作的原子性。

但实际上虚拟机实现一般把这些操作实现为具有原子性的操作,因此在编写代码时一般不需要把用到的long、double变量声明为volatile.

4.Java内存模型的特征——原子性、可见性、有序性

(a).原子性

由Java内存模型直接保证的原子性变量操作包括read、load、assign、use、store、write。

大范围的原子性保障可以使用lock、unlock(synchronized)。

(b).可见性

volatile关键字;final关键字;synchronized。

(c).有序性

如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的(指令重排序)。volatile;synchronized.

四、先行发生原则

happens-before原则是判断数据是否存在竞争、线程是否安全的主要依据。依据这些规则,可以解决并发环境下两个操作之间是否可能存在冲突的问题。

先行发生是Java内存模型中定义的两项操作之间的偏序关系。如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响被操作B察觉。影响指修改了共享内存的值、发送了消息、调用了方法等。

以下列出Java内存模型天然的先行发生原则:

  • 程序次序规则。在一个线程内,书写在前面的代码先行发生于后面的。确切地说应该是,按照程序的控制流顺序,因为存在一些分支结构。
  • 管程锁定规则。一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调是同一个锁。
  • Volatile变量规则。对一个volatile修饰的变量,对他的写操作先行发生于读操作。
  • 线程启动规则。Thread对象的start()方法先行发生于此线程的每一个动作。
  • 线程终止规则。线程的所有操作都先行发生于对此线程的终止检测。
  • 线程中断规则。对线程interrupt()方法的调用先行发生于被中断线程的代码所检测到的中断事件。
  • 对象终止规则。一个对象的初始化完成(构造函数之行结束)先行发生于发的finilize()方法的开始。
  • 传递性。操作A先行发生B,B先行发生C,那么,A先行发生C。

注意: 时间上的先发生,与先行发生原则之间基本没有太大关系,所以衡量并发性问题时,一切以先行发生原则为准。

三、Java线程

一、线程的实现

这里写图片描述

实现线程的方式有三种:使用内核线程实现、使用用户线程实现和使用用户线程加轻量级进程混合实现。

1.使用内核线程实现(KLT)

内核线程是直接由操作系统内核支持的线程,这种线程由内核完成线程切换,内核通过调度器(Scheduler)对线程进行调度,并负责将线程任务映射到各个处理器上。

程序一般使用内核线程的一种高级接口——轻量级进程(LWP),每个轻量级进程都有一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。一对一的线程模型(轻量级进程与内核线程1比1关系):
这里写图片描述

优缺点:
优点: 由于内核线程的支持,每个轻量级进程都是一个独立的调度单元。即使有一个轻量级进程在系统调度中阻塞,也不会影响整个进程的工作。

缺点:①由于基于内核线程实现,各种线程操作,如创建、析构、同步,都需要系统调用,代价很高,需要在用户态与内核态切换。②每个轻量级进程都有一个内核线程支持,轻量级进程要消耗一定的内核资源,一个系统支持的轻量级进程数量有限。

2.使用用户线程实现

从广义上说,一个线程只要不是内核线程,就是用户线程。因此,轻量级进程也属于用户线程,但轻量级进程的实现始终建立在内核线程之上,许多操作都要进行系统调用,效率会受到限制。

狭义上的用户线程指完全建立在用户空间上的线程库上,系统内核不能感知线程存在的实现。用户线程的建立、同步、销毁和调度完全在用户台湾菜,不需要内核的帮助。

这里写图片描述

优缺点:
(a)优点:操作高效、低耗,支持规模更大的线程数量,为1对多的线程模型。
(b)缺点:所有的线程操作需要用户程序处理,实现比较复杂(创建、切换和调度);

3.使用用户线程+轻量级进程实现

这里写图片描述

①用户线程完全建立在用户空间上,因此其创建、切换等操作依然廉价,并且支持大规模的用户线程并发。②操作系统提供的轻量级进程作为用户线程与内核线程的桥梁,这样可以使用内核提供的线程调度和处理器映射,并且用户线程的系统调用要通过轻量级进程实现,大大减低了整个进程被阻塞的风险。

4.Java线程的实现

二、Java线程的调度

线程调度是系统为线程分配处理器使用权的过程。分为协同式线程调度、抢占式线程调度。

(1)协同式线程调度:线程执行时间由线程本身来控制,执行完后主动通知系统切换到另一个线程。实现简单,容易造成阻塞。

(2)抢占式调度:每个线程由系统分配执行时间,由系统控制线程执行时间(Java使用的线程调度方式)。

三、状态转换

这里写图片描述

阅读全文
0 0
原创粉丝点击