Java内存模型
这篇文章,是对Java并发编程系列知识整理的开始,那么开始吧。
Java内存模型的基础
在学操作系统中,便知道进程与线程,线程之间是可以共享进程资源的。在Java语言以及JVM中,线程就是运行的最小单位了,在JVM中,对应着栈——数据结构。两个问题:
1.线程之间是如何通信的?
2.线程之间是如何同步的?
可选择项,共享内存和消息传递,Java选择了共享内存。意味着线程与线程之间是相互独立的,没有直接的消息传递。
什么是同步?程序中用于控制不同线程间操作发生的相对顺序的机制,比如生产者和消费者机制,消费者要等生产者先生产,没有则等待。生产者要生产,但是不能无限的生产,存储满了,要等消费者消费。
Java采用共享内存模型,通过共享内存隐式通信。在Java,实例域,静态域和数组元素都存储在堆内存中 ,这些可以是共享变量。局部变量,形参和异常处理参数不会在线程间共享。JMM(Java Momery Model),控制对一个线程对共享变量的写入, 何时对另一个线程可见。线程运行时,会有自己的本地内存的(JVM中的栈,CPU的寄存器什么的),JVM的堆充当主内存,共享变量应在主内存中。
重排序
在微观视角,机器运行角度,软件技术和硬件技术的共同目标,是在不改变程序执行结果的前提下,尽可能提高并行度。编译器,系统指令,内存系统都会进行优化,进行重排序。编译器会优化重排序,指令级会并行重排序,内存系统会重排序。
怎么限制这些重排序?内存屏障指令,禁止相关重排序。
JSR-133使用happens-before描述内存可见性。
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
- 监视器规则:对一个锁的解锁,happens-before对于这个锁的加锁。
- volatile变量规则:对一个volatile域的写,happens-before于对这个volatile域的读。
- 传递性:如果A happens-before B, B happens-before C,那么A happens-before C。
顺序一致性
同步原语 synchronized,volatile,final。
顺序一致性模型保证单线程内的操作会按程序顺序执行,JMM不保证单线程会按程序顺序执行。
顺序一致性模型保证所有线程只看到一致的操作执行顺序,JMM不保证所有线程能看到一致的操作执行顺序。
JMM不保证64位的long和double型变量的写操作具有原子性。
volatile的内存语义
可见性和原子性,复合操作不具有原子性。
当写一个volatile变量时,JMM会把该线程对应的本地内存中的值刷新到主内存中。
当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,线程从主内存中读取。
锁的内存语义
锁是并发编程同步机制的基础,让临界区互斥执行。
线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
线程获取锁时,JMM会把该线程对应的本地内存设置位无效,从主内存中获取。
锁和volatile的内存语义是相同的。
在Java,锁依赖于同步器框架AbstractQueuedSynchronizer,简称AQS。AQS提供模板方法供使用(模板设计模式),具体的锁只要实现定义好的接口即可。
比如ReentrantLock,是基于AQS实现的重入锁。在AQS中,有个volatile int state,来实现锁的量化。具体的细节,不在这里细说了,会单独分析一下Java源码。在AQS实现加锁,叫compareAndSetState方法,最终交由unsafe.compareAndSwapInt本地方法实现,我们简称为CAS。
公平锁和非公平锁的分区?
简单分析源码,对于公平锁,在进行CAS之前,在AQS中的队列,获取最前面的,判断是否为当前线程,如果不是,就不会获取锁。对于非公平锁,简单很多,直接进行CAS,如果成功,设置锁的当前线程。如果CAS失败,会调用acquire方法。acquire方法(即模板方法),在AQS里定义,子类需要实现tryAcquire方法。如果tryAcquire返回true,则acquire调用中断方法。如果tryAcquire返回false(意味着没有获取锁),则调用acquireQueued方法。acquireQueue方法,具体的锁类来实现。
一个锁的通用实现模式:
volatile变量,CAS原子条件更新同步,配合volatile的读写的内存语义实现线程之间的通信。
final域的内存语义
- 构造函数中对final域的写入,与后续赋值给另一个变量,不能重排序
- 初次读一个包含final域的对象的引用,与随后初次读这个final域,不能重排序
- JMM禁止编译器把final域的写重排序到构造函数之外。
happens-before
对于程序员,希望内存模型易于理解,易于编程。编译器,处理器希望内存模型对于它们的约束越少越好,尽可能提高并行能力。Java用happens-before规则来权衡。对于会改变程序执行结果的重排序,JMM要求编译器和处理器禁止重排序。对于不会改变程序执行结果的重排序,JMM不做要求。
什么是happens-before?如果一个操作happens-before另一个操作,那么第一个操作的执行结果对第二个操作可见,而且第一个操作的执行顺序在第二个之前。那么两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序执行,在不影响执行结果下,是可以重排序的。
happens-before规则,
程序顺序规则:在一个线程中的每个操作happens-before与该线程中的任意后续操作。
监视器规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile变量的读。
传递性:省略。
start规则:如果线程A执行操作ThreadB.start(),那么A线程的ThreadB.start操作happens-before于线程B的任意操作。
join规则:如果线程A执行操作ThreadB.join(),并成功返回。那么线程B中的任意操作happens-before于线程A从ThreadB.join操作成功返回。
双重检查锁定与延迟初始化
单例模式,进行延迟加载和初始化,如果设计的不好,会有多线程并发问题以及性能方面的考虑。这里只是文字描述一下,后续整理设计模式时,将贴上对应的代码。
- 静态变量+普通方法,会有多线程并发问题。
- 静态变量+同步方法(synchronized),没有问题,性能不优。
- 静态变量+double check方法,能保证只有一个线程new,但是另一个线程可能获取到没有初始化完成的引用。new指令,会被拆分成三部。分配内存,初始化对象,设置引用指向分配的内存。但是第二步和第三步可能会被重排序,存在风险,使用volatile进行规避。
- volatile静态变量+double check方法,完美,需要JDK1.5或更高版本支持。
- 静态方法+私有静态内部类(包含一个new的实例),同样可以。类初始化保证线程安全。
切记:内存模型是逻辑上的概念,JVM的实现是灵活的,厂商根据JVM规范来实现。