java线程之间的通信由java内存模型控制,Java内存模型(JMM)决定一个线程对共享变量的写入何时对其他线程可见,为了更好的学习java多线程,我们有必要了解一下java内存模型,在了解java内存模型之前,需要先了解一些其他必要的概念。
指令重排序
在执行程序时,为了提高性能,编译器和处理器可能对指令做重排序。但是,JMM确保在不同的编译器和处理器上,通过插入特定类型dMemory Barrier来禁止特定类型的编译器重排序和处理器重排序,为上层提供一致的内存可见性保证。
编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
指令级并行的重排序:如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
内存系统的重排序:处理器使用缓存和读取缓冲区,使得加载和存储操作看上去可能在乱序执行。举个指令重排序的例子
public class PossibleReordering { static int x = 0,y = 0; static int a = 0, b = 0; public static void main(String[] args) throws InterruptedException { Thread one = new Thread(new Runnable() { @Override public void run() { a = 1; x = b; } }); Thread other = new Thread(new Runnable() { @Override public void run() { b = 1; y = a; } }); one.start(); other.start(); one.join(); other.join(); System.out.println("("+x+","+y+")"); } }结果可能输出(1,0)或者(0,1),(1,1):线程A可以在线程B开始之前执行,线程B也可以在线程A开始之前执行,或者 两者的操作可以交替执行的,但是其实结果还有可能输出(0,0),因为每个线程中各个操作没有数据依赖性,因此其实这些操作可以是乱序执行的,比如下图所示
内存屏障(Memory Barrier)
内存屏障,又称为内存栅栏,是一条CPU指令,它是这样的一条指令:
a)保证特定操作的执行顺序。b)影响某些数据的内存可见性
因为编译器和处理器为了性能优化,会尝试进行指令重排,但是插入一条Memry Barrier会告诉编译器和处理器:这条Memory Barrier指令不与任何其他指令重排序..Memory Barrier可以强制刷出CPU cache,例如一个write-Barrier将刷出所有在Barrier之前写入数据,因此,任何其他线程都可以这些数据的最新值,volatile就是基于Memory Barrier实现的。如果一个变量是volatile修饰的,JMM会在写入这个字段之后插入一个Write-Barrier,并在读取这个字段之前插入一个Read-Barrier指令。保证线程修改变量的值后,会被立即写入到内存,同时保证其他线程可以读到最新的值。
数据依赖性
如果两个操作访问同一个变量,其中一个是写操作,那么这两个操作存在数据依赖性,编译器和处理器不会改变存在依赖性的两个操作,即不会进行执行重排序
as-if-serial
不管怎么重排序,单线程下的执行结果不会改变,编译器和处理器包括运行时都得遵守这个规则。
Happens-Before
JMM为程序中所有的操作定义了一个偏序关系,称为Happens-Before,要想保证执行操作B的线程看到操作A的结果(无论A和B是否在一个线程内),那么A和B之间必须满足Happens-Before关系,如果两个操作缺乏这种关系,那么JVM可以对它们进行重排序。Happens-Before规则包括
程序顺序规则:如果程序中操作A在操作B之前,那么线程中A操作将在B操作操作之前执行。
监视器锁规则:在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前执行
volatile变量规则:对volatile变量的写入操作必须在该变量的读操作之前执行
线程启动规则:在线程上对Thread.start的调用必须在该线程中执行任何操作之前执行
线程结束规则:线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行,或者从Thread.join中成功返回,或者在调用Thread.isAlive时返回false
中断规则:当一个线程在另一个线程上调用interrupt时,必须在被中断线程检测到interrupt调用之前执行(通过抛出InterruptedException,或者调用isInterrupted和interrupted)
终结器规则:对象的构造函数必须在启动该对象的终结器之前执行完成
传递性:如果操作A在操作B之前执行,并且操作B在操作C之前执行,那么操作A必须在操作操作C之前执行。
下图给出了当两个线程在使用锁进行同步时,在它们之间的Happens-Before关系,线程A内部的所有操作都按照它们在源程序中的先后顺序执行,在线程B内部操作也是如此,由于A释放了锁M,并且B随后获得了锁M,因此A中所有在释放之前的操作,也就位于B中请求锁之后的所有操作之前,如果这两个线程在不同的锁同步,那么不能推断它们的动作顺序。
JMM定义了线程和主内存之间的抽象关系:线程之间共享的变量存在于内存之中,每个线程都有自己的本地内存,存储了该线程的共享变量副本,线程A和B如果要通信的话,必须经过两个步骤:线程A把本地内存中更新的共享变量刷新到内存中,同时线程B到内存中去读取线程A之前更新的变量。