Java语言的其中一个特点为跨平台性即由Java编写的程序,一次编译后就可以在多个系统平台上运行。
正式Java虚拟机中存在JMM(Java Memory Mode),才可以实现让Java达到一次编译,处处运行的效果。
一、硬件内存结构
由于计算机中的存储内存与处理器的运算速度有这量级的差距,因此计算机不得不在主内存和处理器之间加入高速缓存来作为存储内存与处理器之间的缓冲区域。高速缓存的读写速度接近于处理器的运算速度,因此在实际的程序运算中,会将运算需要用到的数据从主内存中复制到缓存中,处理器读取缓存中的数据即可进行运算,运算结束后高速缓存再讲运算结果同步到主内存中,完成计算机中的运算和读取操作。
硬件内存结构如下图,可以看到每个处理器都有自己的高速缓存,那么在这些缓存同步到主内存时会导致缓存之间的数据不一致性。为了解决数据不一致性,在处理器执行操作时会使用MSI、MESI、MOSI等协议来保证他们之间的缓存一致性(在Java虚拟机中也会有类似的问题,一般是使用关键字和happens-before原则来保证一致性)。
为了保证处理器运算效率和保证处理器内部单元可以被充分利用,处理器会对输入的程序进行乱序排序优化,在计算完成后再重组结果(在Java中也会有类似的重排策略)。
二、JMM
为了保证JMM可以屏蔽硬件中内存和处理器的差异,所以可以发现Java的内存模型与硬件的内存结构是类似的,在JMM中也存在主内存、工作内存(高速缓存)、处理器线程(处理器)。并且在JMM中也存在重排策略,并且比硬件中的重排策略优化性更高、更加细致。
Java内存模型结构如下图:
JMM与硬件内存结构相似,他规定了所有的变量都存储在主内存中,所有的运算处理都在线程中完成,并且每条线程都有自己的工作内存,在工作时每个工作内存保存了线程所需要用到的变量的拷贝。
三、JMM的实现
JMM的实现是为了保证在并发情况下保证线程的原子性、可见性、有序性。JMM提供了一系列的关键字(synchronized、volatile、final等)来保证。
· 原子性
原子性表示线程的操作不可中断,要么全部成功,要么全部失败。在JMM中使用了关键字synchronized来保证线程操作的原子性。
synchronized关键字后面会专门作为知识点来写,简单来说就是在虚拟机中synchronized是根据指令来执行的,底层的指令分为monitorer和monitorexit,当线程遇到monitorer指令时线程会尝试回去锁,那么锁计数器会+1.如果没有锁就会被阻塞。当线程遇到monitorexit指令时,锁计数器-1,当计数器为0时,表示同步块执行,释放锁资源。但是如果连续遇到了2次monitorexit时表示线程是遇到了异常而释放的锁。
· 可见性
可见性表示一个线程修改了变量值后可立即将新值同步到共享内存中。在JMM中提供了关键字volatile、synchronized来保证线程操作的可见性。
volatile关键字已经专门作为知识点来写了,如果想要了解可以查看历史文章记录。简单来说是因为被volatile关键字修饰的变量可以保证该变量被修改后被立马同步到主内存中,且每次使用时都刷新该变量的值。普通变量由于需要经过工作内存到主内存的同步,因此普通变量的修改无法保证可见性。
synchronized关键字可以保证可见性是因为在同步块中只有获得锁资源的线程才可以执行方法,保证每次都会把变量修改同步到主内存中。
· 有序性
有序性表示在线程中所有的操作都是有序的。但是在多线程环境下,重排策略会导致在多线程中是无序的。JMM中提供了关键字volatile、synchronized关键字来保证线程的有序性。
volatile关键字是因为被volatile修饰的变量被修改后会立马同步到主内存中,所以他本身包含了禁止重排策略的语义。
synchronized关键字是因为在该同步块中只有获得锁的线程才能执行,当多线程情况下,可以保证被程序串行的来运行。
· JMM重排策略
(1)编译器重排:编译器在不改版程序语义的情况下,会重新安排语句的执行顺序。
(2)指令并行重排:在不存在语句依赖关系的情况下,处理器会重新安排语句的执行顺序。
(3)内存系统的重排:处理器和主内存中由于工作内存的存在会导致缓存与内存的同步存在时间差。
在平时编写代码时我们并不是一定是需要使用这些关键字来保证程序的有序性,并且在程序运行时重排策略对我们是无感知的,这是因为我们在编写代码时,需要遵循happens-before原则来帮助我们辅助线程的原子性、可见性、有序性。
· happens-before原则
(1)程序次序原则:在一个线程内必须保证程序语义的串行性,按照代码顺序执行。
(2)管理锁定原则:对同一个锁需要先解锁再加锁,即加锁动作必须在解锁之后。
(3)volatile原则:对volatile变量的写操作,先发生于后面对这个变量的读操作。
(4)线程启动规则:Thread对象的start()方法执行先行发生于该Thread线程对象的所有运行动作之前。
(5)线程终止规则:线程中所有操作都先行发生于线程的终止检测。即通过Thread.join()、Thread.isAlive()方法检测线程是否终止前必须保证线程操作都已结束。
(6)线程中断规则:对线程interrupt()中断方法的调用先行发生于中断检测事件之前,可以通过Thread.interrupted()方法检测线程是否中断。
(7)对象终结原则:一个对象的初始化完成(构造函数执行结束)先行发生于他的finalize()方法之前。
(8)传递性,如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A必然先行发发生于操作C。
四、总结
JMM模型中允许主内存和工作线程中存在运行性能与工作线程相似的高速缓存。主内存中的数据是所有线程共享的,而线程中进行运算时需要将主内存中的数据复制到缓存中运算结束后再将运行结果同步到主内存中。在高并发的环境下,这样的工作过程和重排策略会导致线程安全问题。
在JMM中提供了Java关键字来保证线程的原子性、可见性、有序性。对于方法级别或者代码块级别的原子性问题,可以通过synchronized关键字来保证线程执行的原子性。在工作内存和主内存中同步数据存在延迟导致的可见性问题可以通过synchronized和volatile关键字来保证线程执行的可见性。对于指令重排导致的有序性问题,可以通过synchronized和volatile关键字来保证线程执行的有序性问题。
在编写代码时,我们也可以使用JMM内部定义的happens-before原则来辅助我们保证线程执行的原子性、可见性、有序性。