Java内存模型
Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
Java内存模型的抽象示意图如下:
从上图来看,线程A和线程B之间进行通信必须经历两个步骤:
1、线程A把本地内存的共享变量刷新到主内存,
2、线程B通过读取主内存中之前线程A更新的变量。
下面通过示意图来说明这两个步骤:
如上图所示,线程A和B有主内存共享变量x的副本。假设一开始三个内存中的x=0,此时线程A需要与线程B通信。
线程A在执行时,将本地内存的变量x改为1,然后刷新到主内存中,主内存x=1;
此时线程B通过去主内存中读取之前线程A刷新的变量x=1,线程B的本地内存变量x也变为1。
从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。
JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序提供内存可见性保证。
JMM 保证线程安全是围绕原子性、可见性、有序性这 3 个特性来建立的。
体贴的 JMM 在满足这 3 个特性的时候给了很多关键字或预定规则,比方说,在原子性中的 8 种操作,这样我们在操作基本数据类型的读和写就可以看成是线程安全的。
其中的 lock 和 unlock 指令在 Java 语言中的体现就是 synchronized 关键字,所以 synchronized 块之间的操作也是原子性的。
可见性的体现,volatile 关键字,上面已经说过了,还有两个关键字 synchronized 和 final。
因为被 synchronized 包围的代码被线程执行的前提是要 lock,对应的有条规则说到“对一个变量执行 unlock 之前,要先把其写回主存”。
而 final 定义的变量,一旦初始化完成,其它线程都能看到(当然这里假设不出现对象逃逸,就是不会在对象初始化没有完成的时候被其它线程拿到 this 对象进行操作。)
最后一个有序性,提供了关键字 volatile 和 synchronized,volatile 禁止重排序来达到有序,而 synchronized 则是基于 “一个变量在同一时刻只允许一条线程对其进行 lock 操作,lock 和 unlock 必须成对出现” 这个规则。
另外,JMM 为了保证有序,还内置了一套先行发生规则(happens-before)两个操作间具有 happens-before 关系,并不意味着前一个操作必须要在后一个操作之前执行。happens-before 仅仅要求前一个操作对后一个操作可见,和一般意义上时间的先后是不一样的,达到逻辑上的顺序执行即可。
如果 A 线程的写操作 a 与 B 线程的读操作 b 之间存在 happens-before 关系,尽管 a 操作和 b 操作在不同的线程中执行,但 JMM 向程序员保证 a 操作将对 b 操作可见。我们实际只想知道某线程的操作对另一个线程是否可见,于是就规定了 happens-before 这个可见性原则,程序员可以基于这个原则进行可见性的判断。
具体的规则如下:
1、程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生与书写在后面的操作。【保证单线程的有序】
2、锁定规则:一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
3、volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。【先写后读】
4、传递规则:A 先于 B 且 B 先于 C 则 A 先于 C
5、线程启动规则:Thread 对象的 start 方法先行发生于此线程的每一个动作。
6、线程中断规则:对线程 interrupt 方法的调用先行发生于被中断线程的代码检测到中断事件的发生。【先中断,后检测】
7、线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过 Thread.join() 方法结束,Thread.isAlive() 的返回值手段检测线程已经终止执行。
8、对象终结规则:一个对象的初始化完成先行发生于它的 finalize 方法的开始。
如果两个操作的执行顺序不能通过 happens-before 原则推导出来,就不能保证他们的执行次序,虚拟机就可以随意的对他们进行重排序。
更多内容关注公众号>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>2021最新面试题