java内存模型定义了线程和内存之间的关系,线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存存储了该线程以读/写共享变量的副本。本地内存是 JMM 的一个抽象概念,并不真实存在;它涵盖内存、缓存、寄存器以及其他的硬件和编译器优化。Java的内存模型抽象如下:
因此,如果有人问你,java多线程中是通过什么机制进行线程通信和数据同步的,通过以上图可以得出结论,是通过共享内存模型来实现多线程之间的通信和数据同步的。
(ps:可以类比,比如说一个电脑上有4个cpu,每个cpu上各自有自己的缓存,然后各自跑一个线程,这个线程的数据都在这个cpu的缓存上,它并不知道其他cpu上发生了什么,如果想知道,只有当其他cpu将操作结果放到内存中的时候,它去内存中读取到才能知道。)
Java内存模型是围绕着并发编程中原子性、可见性、有序性这三个特征来建立的。
原子性(Atomicity):一个操作不能被打断,要么全部执行完毕,要么不执行。在这点上有点类似于事务操作,要么全部执行成功,要么回退到执行该操作之前的状态。
可见性:一个线程对共享变量做了修改之后,其他的线程立即能够看到(感知到)该变量这种修改(变化)。
Java内存模型是通过将在工作内存中的变量修改后的值同步到主内存,在读取变量前从主内存刷新最新值到工作内存中,这种依赖主内存的方式来实现可见性的。
无论是普通变量还是volatile变量都是如此,区别在于:volatile的特殊规则保证了volatile变量值修改后的新值立刻同步到主内存,每次使用volatile变量前立即从主内存中刷新,因此volatile保证了多线程之间的操作变量的可见性,而普通变量则不能保证这一点。
除了volatile关键字能实现可见性之外,还有synchronized,Lock,final也是可以的。
使用synchronized关键字,在同步方法/同步块开始时(Monitor Enter),使用共享变量时会从主内存中刷新变量值到工作内存中(即从主内存中读取最新值到线程私有的工作内存中),在同步方法/同步块结束时(Monitor Exit),会将工作内存中的变量值同步到主内存中去(即将线程私有的工作内存中的值写入到主内存进行同步)。
使用Lock接口的最常用的实现ReentrantLock(重入锁)来实现可见性:当我们在方法的开始位置执行lock.lock()方法,这和synchronized开始位置(Monitor Enter)有相同的语义,即使用共享变量时会从主内存中刷新变量值到工作内存中(即从主内存中读取最新值到线程私有的工作内存中),在方法的最后finally块里执行lock.unlock()方法,和synchronized结束位置(Monitor Exit)有相同的语义,即会将工作内存中的变量值同步到主内存中去(即将线程私有的工作内存中的值写入到主内存进行同步)。
final关键字的可见性是指:被final修饰的变量,在构造函数数一旦初始化完成,并且在构造函数中并没有把“this”的引用传递出去(“this”引用逃逸是很危险的,其他的线程很可能通过该引用访问到只“初始化一半”的对象),那么其他线程就可以看到final变量的值。
有序性:对于一个线程的代码而言,我们总是以为代码的执行是从前往后的,依次执行的。这么说不能说完全不对,在单线程程序里,确实会这样执行;但是在多线程并发时,程序的执行就有可能出现乱序。用一句话可以总结为:在本线程内观察,操作都是有序的;如果在一个线程中观察另外一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行语义(WithIn Thread As-if-Serial Semantics)”,后半句是指“指令重排”现象和“工作内存和主内存同步延迟”现象。
Java提供了两个关键字volatile和synchronized来保证多线程之间操作的有序性,volatile关键字本身通过加入内存屏障来禁止指令的重排序,而synchronized关键字通过一个变量在同一时间只允许有一个线程对其进行加锁的规则来实现,
在单线程程序中,不会发生“指令重排”和“工作内存和主内存同步延迟”现象,只在多线程程序中出现。
happens-before原则:
Java内存模型中定义的两项操作之间的次序关系,如果说操作A先行发生于操作B,操作A产生的影响能被操作B观察到,“影响”包含了修改了内存中共享变量的值、发送了消息、调用了方法等。
下面是Java内存模型下一些”天然的“happens-before关系,这些happens-before关系无须任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们进行随意地重排序。
a.程序次序规则(Pragram Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环结构。
b.管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而”后面“是指时间上的先后顺序。
c.volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读取操作,这里的”后面“同样指时间上的先后顺序。
d.线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。
e.线程终于规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值等作段检测到线程已经终止执行。
f.线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否有中断发生。
g.对象终结规则(Finalizer Rule):一个对象初始化完成(构造方法执行完成)先行发生于它的finalize()方法的开始。
g.传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。
一个操作”时间上的先发生“不代表这个操作会是”先行发生“,那如果一个操作”先行发生“是否就能推导出这个操作必定是”时间上的先发生 “呢?也是不成立的,一个典型的例子就是指令重排序。所以时间上的先后顺序与happens-before原则之间基本没有什么关系,所以衡量并发安全问题一切必须以happens-before 原则为准。
volatile的语义
volatile主要提供了两种语义:
1,可见性:
可见性是指一个线程写入的值,其他线程能够立即读取。在由Java内存模型可知道,每个线程都是有本地内存。所以线程A写入在正常情况下,线程B不能立即读取。但是在volatile变量,可以保证线程A不写入本地内存直接写入主内存,线程B直接从主内存中读取,不从本地内存中读取。
2,禁止指令重排序:
重排序是指编译器和处理器为了优化程序性能而对指令进行重排序的一种优化手段。
Java程序的几种重排序
编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
指令级并行的重排序:如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
内存系统的重排序:处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行
volatile的技术基石–内存屏障
内存屏障是cpu指令,该指令保证特定操作的顺序性和某些内存的可见性。插入一条内存屏障指令之后会告诉编译器和CPU:不管什么指令都不能和这条指令重排序。内存屏障所做的另外一件事情就是强制刷出各种CPU cache,如一个Write-Barrier(写入屏障)将刷出所有在Barrier之前写入cache的数据,因此,任何CPU上的线程都能读取到这些数据的最新版本。
对于Java程序而言,如果把加入volatile关键字的代码和未加入volatile关键字的代码都生成汇编代码,会发现加入volatile关键字的代码会多出一个lock前缀指令。
volatile的典型用例
状态标志,代码示例如下:
线程1执行run()的过程中,可能有另外的线程2调用了shutdown,所以stop变量必须是volatile(利用的volatile的可见性)。
还有一种常见的用法在双重检验的单例实现上,代码如下:
**instance = new Singleton()**这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:
1.给 instance 分配内存
2.调用 Singleton 的构造函数来初始化成员变量
3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
如果instance变量没有加volatile,因为指令重排序的存在,就可能导致执行步骤是1-2-3,也可能是1-3-2。一旦是1-3-2,就可能会导致访问未初始化的内存。但是加上volatile关键字之后,一定保证是按照1-2-3步骤执行的(利用的volatile的禁止重排序)。