随着现代CPU从提升单核性能转变到提升多核性能,并发编程显得愈发重要。通过多线程的协同工作使得程序的执行效率更高!
同时,计算机软硬件为了提升程序执行效率,对指令做了重排序操作,分为三种类型:
- 编译器优化的重排序:编译器在不改变单线程程序的前提下进行指令的重排序;
- 指令级并行重排序:现代CPU采用了指令级并行技术,包括流水线技术以及针对指令顺序进行重排序的方式;
- 内存系统的重排序:由于处理器使用了读/写缓冲区,使得加载和存储操作看起来是乱序执行的。
Java内存模型JMM是一个抽象概念,涵盖了缓存、写缓冲区、寄存器以及其它硬件和编译器优化,用于解决Java多线程对共享数据的读写一致性问题。在JMM中变量的值从线程A到线程B要经过两个阶段:线程A将工作内存中更新过的共享变量刷到主内存中;线程B从主内存中读取线程A更新过的变量。
基于上述软硬件层面的重排序操作以及JMM内存模型共享数据的读写方式,就有可能带来数据同步问题。也就是说写变量的位置不一定早于读变量位置。
对于理想化的顺序一致内存模型来说,其具备两个特性:
- 一个线程中的所有操作必须按照程序的顺序来执行(无各种指令重排序);
- (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序(每个操作都必须原子执行且立刻对所有线程可见,大概就是全是volatile变量)
因此,我们可以说,在理想的一致性内存模型中,是不存在数据同步问题的,但是存在性能问题。所有操作被串行化了,没有使用到CPU的多核特性。
JMM为了保证确保对CPU多核特性的充分利用,提高程序执行的效率,针对未正确同步的程序不具备上述内存模型特征:
- 不保证单线程内操作按程序顺序执行(重排序);
- 不保证所有线程看到一致的操作执行顺序(内存可见性,指令重排序等);
- 不保证对64位long或double型变量写操作具有原子性(JDK5之前读/写long或double不具备原子性,JDK5开始读具备原子性),而顺序一致性模型保证对所有内存读/写操作都具有原子性。
Java通过happens-before
来指定两个操作之间的执行顺序,保证某个操作一定先发生于另一个操作。对程序员来说确保了编程时内存模型的可靠性;对于编译器和处理器而言,对其优化重排序起到约束作用。happens-before有且只有以下八种:
- 程序顺序规则:同一个线程前面的操作happens-before其后面任意操作;
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁(
锁的内存语义
); - volatile变量规则:对volatile变量的写,happens-before后续任意对这个变量的读(
volatile内存语义
); - 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C;
- start()规则:线程A执行ThreadB.start(),那么线程A的ThreadB.start() happens-before线程B中任何操作(线程A在ThreadB.start()之前对共享变量所做的修改,在线程B开始执行后都将对其可见);
- join()规则:线程A执行ThreadB.join(),那么线程B的任何操作happens-before线程A从ThreadB.join()操作成功返回(线程A执行ThreadB.join()并成功返回后,线程B的任何操作对线程A可见);
- 线程中断规则:调用线程A的interrupt()方法happens-before线程A检测到中断事件的发生;
- 对象终结原则:对象的初始化完成(构造函数执行完成)happens-before于finalize()方法的开始
注意:A happens-before B不代表A比B先执行,比如下面的A、B,虽然A happens-before B,但由于重排序,B可能比A先执行。因为A的执行结果不需要对B可见。
final的内存语义:写final域的重排序规则:在对象引用为任何线程可见之前,对象的final域已经被正确初始化过了(构造函数)。普通域则可能出现对象引用为线程可见,但该域还未被初始化;读final域的重排序规则:先读包含此final域的对象引用,再读此final域;而针对普通域而言,可能出现未获取对象引用之前的错误读取操作。
主要参考文献:《Java并发编程的艺术》