并发编程模型中的两个关键问题
- 线程通信:通信是指线程之间以何种机制来交换信息,线程之间的通信机制有两种,分别是内存共享和消息传递
- 线程同步:同步是指程序中用于控制不同线程间操作相对顺序的机制
java的并发采用的是内存共享模型,java线程之间的通信总是隐式进行的。
java内存模型的抽象结构
在java中,所有的实例域,静态域和数组元素都存在在堆内存中,堆内存在线程之间共享,局部变量,方法的定义参数和异常处理参数不会在线程之间共享,他们不会有内存可见性问题
从源代码到指令顺序的重排序
在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序,重排序常常分为以下三种类型。
- 编译器优化重排序
- 指令集并行重排序
- 内存系统的重排序
这些重排序可能会导致多线程程序出现内存可见性问题。JMM属于预言机的内存模型,他确保在不同的编译器和处理器平台上,通过禁止特定类型的编译器和处理器重排序,为程序员提供一致的内存可见性保证
并发编程模型的分类
由于写缓存区仅仅对自己的处理器课件,他会导致处理器执行内存操作的顺序和内存的实际顺序不一致,由于现代处理器都会使用缓冲区,因此现代的处理器都会允许对写-读进行重排序
为了保证内存可见性,Java编译器在生成执行序列的适当位置会插入内存屏障来禁止特定类型的处理器重排序,java会把内存屏障指令分为以下的几类:
happens-before
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系,这里的连个操作可以是在一个线程之中,也可以是在不同的线程之间。
与程序员有关的happens-bofere规则如下:
- 程序顺序规则:一个线程的每个操作,happens-before与这个线程中的任意后续操作
- 监视器锁规则:一个锁的解锁,happens-before与这个锁的加锁
- volatile变量规则:对于一个volatile域的写,happens-before任意后续对这个volatile域的读
- 传递性原则,happens-before具有传递性
as-if-serial语义
as-if-serial语义的意思是,不管怎么重排序(编译器和处理器为了提高并行度)程序的执行结果不会改变,为了能够遵守该原则,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果,如果操作之间不存在数据依赖关系,这些操作就能够进行重排序。如果A happens-before B,JMM不要求A一定要在B之前执行,JMM仅仅要求前一个操作的执行结果对后一个操作的执行结果可见
在单线程程序中,对存在依赖控制的操作做重排序是,不会改变执行结果,但是在多线程程序中,对存在依赖控制的操作做重排序,可能会改变程序的执行结果
顺序一致性内存模型
- 一个线程中的所有操作必须按照程序的顺序来执行
- 所有线程都只能看到一个单一的操作执行顺序,在顺序一致性内存模型中,每个操作都必须原子执行并且立即对所有的线程可见
- JMM在不改变程序执行结果的前提下,尽可能为编译器和处理器打开优化的大门。
未同步程序的执行特性
对于未同步或者未正确同步的多线程程序,JMM只提供最小安全性,线程执行时多渠道的值,要么是某个线程写入的值,要么是默认值,JMM保证线程读取到的值不会是无中生有的。
volatile的内存语义
简而言之,volatile变量自身具有以下特性:
- 可见性:对一个volatile变量的读,总能看到对这个volatile变量最后的写入
- 原子性:对于任意单个volatile变量的读写具有原子性,但是类似于volatile++这种符合操作不具有原子性
volatile 写读建立的happens-before关系
从内存语义的角度来说,volatile的写读与锁的释放获取有着相同的内存效果,volatile写和锁的释放有相同的内存语义,volatile读和锁的获取有相同的内存语义
- volatile写的内存语义:当一个volatile变量写入时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存中,
- volatile读的内存语义:当读一个voilatile变量时,JMM会把该线程中对应的本地变量设置为无效,线程接下来将会从主内存中读取共享变量
volatile内存语义的实现
重排序会分为编译器重排序和处理器重排序,为了实现volatile语义,JMM会限制两种重排序类型,
举例来说,当一个操作是volatile读时,不管第二个操作是什么,都不能重排序,这个规则保证了volatile读之后的操作不会被编译器重新排序到volatile读之前
当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序,这个规则保证了volatile 写之前的操作不会被编译器重新排序到volatile写之后
为了实现volatile的内存语义,编译器在实现生成字节码是,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,
上面的volatile写和volatile读的内存屏障插入策略非常的保守,在实际执行的时候,只要不改变volatile写-读的内存语义,编译器可以根据具体的情况省略不必要的屏障
锁的内存语义
锁是java并发编程中最重要的同步机制,锁除了让临界区互斥的执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息
当线程释放锁时,JMM会把线程对应的本地内存中的共享变量刷新到主内存中
当线程获取锁时,JMM会把线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量
锁和volatile有着相同的内存语义
锁内存语义的实现
ReentrantLock的实现依赖于Java同步框架AQS,AQS使用一个整形的volatile变量来维护同步状态。这个volatile变量是ReentrantLock内存语义实现的关键
公平锁加锁是首先读volatile变量state,在释放锁时最后写volatile变量state,根据valatile的happens-before规则,释放锁的线程在写volatile变量之前可见的共享变量,在获取锁的线程获取同一个volatile变量后会立即变得对获取所得线程可见
非公平锁的释放和公平锁的释放方法是一样的,主要介绍非公平锁的加锁
加锁时使用CAS跟新volatile变量state,CAS是同时具有volatile读和volatile写的语义的
经过上面的分析,我们可以总结得到一下的结论,锁释放-获得的内存语义的实现至少有以下两种实现的方式
- 利用voilatile变量的读写所具有的的内存语义
- 利用CAS所附带的内存语义
final域的重排序规则
写final域的重排序规则禁止吧final域的写重排序到构造函数之外,这个规则的实现包含了以下两个方面
- JMM禁止编译器吧final域的写重排序到构造函数之外
- 编译器会在final域的写之后,构造函return之前,插入一个storestore屏障,这个屏障禁止把final的写重排序到构造函数之外
写final域的重排序规则可以保证,在对象引用为任意程序可见之前,对象的final域已经被正确的初始化过了,而普通域不具有这个保证。
读final域的重排序规则
读final域的重排序规则是,在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作,编译器会在读final域操作的前面插入一个LoadLoad屏障
读final域的重排序规则可以确保,在读一个对象的fianl域之前,一定会先读包含这个final域的对象的引用,那么引用对象的final域一定已经被初始化过了
final域为引用对象
在构造函数类对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造函数引用赋值给另一个成员变量,这两个操作之间不能重排序
final引用 不能从构造函数内溢出
在构造函数返回前,被构造对象的引用不能为其他线程所见,因为此时的final域可能还没有被初始化,在构造函数返回后,任意线程都将保证能够看到final域被正确初始化后的值
happens-before
JMM设计的原则:只要不改变程序的执行结果,编译器和处理器怎么优化都可以
happens-before原则增加:
start原则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB 中的任意操作
join原则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before与线程A从ThreadB.join()成功返回
双重检查锁定
在java程序中,有时候需要推迟一下高开小的对此昂初始化操作,并且只有在使用这些处对象时才进行初始化,可以采用双重检查的方法来实现它
但是会重载instance初始化重排序的问题
会使得线程访问到instance时出现没有初始化的问题,可以采用volatile的方法防止重排序