深入理解Java虚拟机- 学习笔记 - Java内存模型与线程

 2019-12-10 11:20  阅读(839)
文章分类:Java Core

除了在硬件上增加告诉缓存之外,为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果一致,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致,因此,如果存在一个计算任务依赖另外一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证。与处理器的乱序优化执行类似,Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Reorder)优化。

Java内存模型

Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model),来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。

主内存与工作内存

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

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

内存间交互操作

关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,Java内存模型中定义了以下8种操作来完成,虚拟机实现时必须保证下面提及的每一种操作都是原子的不可再分的(对于double和long类型的变量来说,load, store, read和write操作在某些平台上允许有例外):

lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。

unlock(解锁): 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

read(读取): 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。

load(载入): 作用于工作内存的变量,它把read操作从主内存得到的变值放入工作内存的变量副本中

use(使用): 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。

assign(赋值): 作用于工作内存的变量,它把一个从执行引擎收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

store(存储): 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。

write(): 作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

如果要把一个变量从主内存复制到工作内存,那就要 顺序的执行read和load操作,如果要把变量从工作内存同步回主内存,就要顺序的执行store和write操作。Java内存模型只要求上述两个操作必须按顺序执行,而没有保证时连续的。如对主内存中a、b进行访问时,一种可能出现的顺序是read a, read b, load b, load a.

Volatile型变量的特殊规则

当一个变量定义位volatile之后,将具备两种特性,第一是保证此变量对所有线程的可见性,第二是禁止指令重排序优化。

可见性

是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。但volatile并不保证i++等非原子操作的并发安全。volatile变量适合与以下两种情况:

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

禁止指令重排序优化

从硬件架构上讲,指令重排序是指CPU采用了允许将多条指令,不按照程序规定的顺序分开发送给各相应电路单元处理但并不是说指令任意重排,CPU需要正确处理指令依赖情况以保障程序能得出正确的执行结果。比如指令1把地址A中的值加10,指令2把地址A中的值乘2,指令3把地址B中的值减3,这时指令1和指令2是有依赖的,它们之间的顺序不能重排,但指令3可以重排到指令1、2之前或者中间。

原子性、可见性与有序性

原子性

Atomicity, 基本数据类型的访问读写是具备原子性的,long和double是例外,因为其是64位的但是绝大多数虚拟机都会对其进行原子性实现,所以基本不用考虑它们非原子性的情况。

可见性

Visibility, 指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。volatile, synchronized和final都能实现可见性。final关键字的可见性是指,被final修饰的字段在构造器中一旦初始化完成,那在其他线程中就能看见final字段的值。

有序性

Ordering, Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。后半句指的是“指令重排序”和“工作内存与主内存同步延迟”现象。

先行发生原则

happens-before,下面是Java内存模型的天然先行发生关系,可以在编码中直接使用。如果两个操作间的关系不在此列或者无法从以下规则中推导出来,虚拟机对它们可以随意的进行重排序。

  • 程序次序规则,Program Order Rule, 在一个线程内,按照程序代码顺序,书写在前面的操作先行发生与书写在后面的操作。
  • 管程锁定规则, Monitor Lock Rule, 一个unlock操作先行发生于后面对同一个锁的lock操作。
  • volatile变量规则,Volatile variable Rule, 对一个volatile变量的写操作先行发生于后面对这个变量的读操作。
  • 线程启动规则,Thread Start Rule, Thread对象的start()方法先行发生于此线程的每一个动作。
  • 线程终止规则,Thead Termination Rule, 线程中的所有操作都先行发生于对此线程的终止检测。可以通过Thread.join()方法的结束和Thread.isAlive()的返回值等手段检测到线程已终止执行。
  • 线程中断规则,Thread Interruption Rule, 对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
  • 对象终结规则,Finalizer Rule, 一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
  • 传递性,Transitivity, 如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

线程安全

线程安全程度

按照线程安全的安全程度由强至弱来排序,我们可以将Java语言中各种操作分享的数据分为以下5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

不可变

Immutable的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施。如果共享数据是一个基本数据类型,那么只要在定义时使用final关键字修饰它就可以保证它是不变的;如果共享数据是一个对象,那就需要保证对象的行为不会对其状态产生任何影响才行。

绝对线程安全

一个类不管运行时环境如何,调用者都不需要任何额外的同步措施。这通常需要付出很大甚至有时候是不切实际的代价。在Java API中标注自己是线程安全的类,大多都不是绝对的线程安全。如多个线程对一个Vector同时添加删除和读取,就可能会遇到ArrayIndexOutOfBoundsException.

相对线程安全

这是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。Java中大部分线程安全类都属于这种类型。

线程兼容

对象本身并不是线程安全的,但是可以通过在调用端正确的使用同步手段来保证对象在并发环境中可以安全地使用。Java中大部分类都属于线程兼容的。

线程对立

无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。非常少见并且是有害的。

线程安全的实现方法

互斥同步

(Mutual Exclusion & Synchronization)是常见的一种并发正确性保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用。互斥是实现同步的一种手段,临界区(Critical Section),互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。

Java的线程是映射到操作系统的原生系统之上的,如果要阻塞或者唤醒一个线程,都需要操作系统来完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间。所以synchronized是一个重量级的操作。ReentrantLock与synchronized很相似,但增加一些高级功能:等待可中断,公平锁,锁可绑定多个条件。公平锁指多个线程在等待同一个锁的时,必须按照申请锁的时间顺序来一次获得锁,非公平锁不保证这一点。synchronized能够实现需求的时候推荐使用synchronized。

非阻塞同步

互斥同步只要问题是进行线程阻塞唤醒所带来的性能问题,因此这种同步也称为阻塞同步(Blocking Synchronization)。从处理问题的方式上说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施就肯定会出问题。基于冲突检测的乐观并发策略,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了,如果由冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重使,直到成功为止)。这种乐观的并发策略的许多实现都不需要将线程挂起,因此这种同步操作称为非阻塞同步(Non-Blocking Synchronization)。

compareAndSwap(CAS指令)在硬件指令层面是一个原子操作。

点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> 深入理解Java虚拟机- 学习笔记 - Java内存模型与线程

相关推荐