1. 初识volatile关键字
这段程序分别启动了两个线程,一个线程负责对变量进行修改,一个线程负责对变量进行输出,根据本书第一部分的知识讲解,该变量就是共享资源(数据),那么在多线程操作的情况下,很有可能会引起数据不一致等线程安全的问题。
import java.util.concurrent.TimeUnit;
public class VoliateFoo {
// init_value的最大值
final static int MAX=5;
// init_value的初始值
static int init_value=0;
public static void main(String[] args) {
// 启动一个Reader线程,当发现local_value和init_value不同时,则输出init_value被修改的消息
new Thread(
()->{
int localValue = init_value;
while(localValue < MAX ) {
if ( init_value != localValue ) {
System.out.printf("The init_value is updated to [%d]\n", init_value);
//
localValue = init_value;
}
}
}
,"Reader").start();
// 启动Updater线程,主要用于对init_value的修改,当local_value>=5的时候则退出生命周期
new Thread(
() -> {
int localValue = init_value;
while(localValue < MAX ) {
// 修改 init_value
System.out.printf("The init_value will be changed to [%d]\n", ++localValue);
init_value = localValue;
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
,"Updater").start();
}
}
运行结果:
Connected to the target VM, address: '127.0.0.1:65218', transport: 'socket'
The init_value will be changed to [1]
The init_value will be changed to [2]
The init_value will be changed to [3]
The init_value will be changed to [4]
The init_value will be changed to [5]
Disconnected from the target VM, address: '127.0.0.1:65218', transport: 'socket'
static int init_value=0; 修改为 static volatile int init_value=0; 添加volatile关键字后运行结果如下:
Connected to the target VM, address: '127.0.0.1:65471', transport: 'socket'
The init_value will be changed to [1]
The init_value is updated to [1]
The init_value will be changed to [2]
The init_value is updated to [2]
The init_value will be changed to [3]
The init_value is updated to [3]
The init_value will be changed to [4]
The init_value is updated to [4]
The init_value will be changed to [5]
The init_value is updated to [5]
Disconnected from the target VM, address: '127.0.0.1:65471', transport: 'socket'
注意:volatile关键字只能修饰类变量和实例变量, 对于方法参数、局部变量以及实例常量, 类常量都不能进行修饰, 比如上面代码中的MAX就不能使用volatile关键字进行修饰。
2.机器硬件CPU
2.1 CPU Cache模型
由于两边速度严重的不对等, 通过传统FSB直连内存的访问方式很明显会导致CPU资源受到大量的限制,降低CPU整体的吞吐量,于是就有了在CPU和主内存之间增加缓存的设计,现在缓存的数量都可以增加到3级了,最靠近CPU的缓存称为L1,然后依次是L2,L3和主内存,CPU缓存模型如图所示。
Cache的出现是为了解决CPU直接访问内存效率低下问题的,程序在运行的过程中,会将运算所需要的数据从主存复制一份到CPUCache中,这样CPU进行计算时就可以直接对CPU Cache中的数据进行读取和写入,当运算结
束之后,再将CPU Cache中的最新数据刷新到主内存当中,CPU通过直接访问Cache的方式替代直接访问主存的方式极大地提高了CPU的吞吐能力,有了CPU Cache之后,整体的CPU和主内存之间交互的架构大致如图所示。
2.2 CPU缓存一致性问题
由于缓存的出现, 极大地提高了CPU的吞吐能力, 但是同时也引人了缓存不一致的问题,比如i++这个操作,在程序的运行过程中,首先需要将主内存中的数据复制一份存放到CPU Cache中, 那么CPU寄存器在进行数值计算的时候就直接到Cache中读取和写人,当整个过程运算结束之后再将Cache中的数据刷新到主存当中, 具体过程如下。
1) 读取主内存的i到CPU Cache中。
2)对i进行加一-操作。
3) 将结果写回到CPU Cache中。
4)将数据刷新到主内存中。
为了解决缓存不一致性问题,通常主流的解决方法有如下两种。
- 通过总线加锁的方式。
- 通过缓存一致性协议。
第一种方式常见于早期的CPU当中, 而且是一种悲观的实现方式, CPU和其他组件的通信都是通过总线(数据总线、控制总线、地址总线)来进行的,如果采用总线加锁的方式, 则会阻塞其他CPU对其他组件的访问, 从而使得只有一个CPU(抢到总线锁) 能够访问这个变量的内存。这种方式效率低下,所以就有了第二种通过缓存一致性协议的方式来解决不一致的问题(见图)。
在缓存一致性协议中最为出名的是Intel的MESI协议, MESI协议保证了每一个缓存中使用的共享变量副本都是一致的, 它的大致思想是, 当CPU在操作Cache中的数据时,如果发现该变量是一个共享变量, 也就是说在其他的CPU Cache中也存在一个副本, 那么进行如下操作:
1) 读取操作, 不做任何处理, 只是将Cache中的数据读取到寄存器。
2) 写入操作, 发出信号通知其他CPU将该变量的Cacheline置为无效状态, 其他CPU在进行该变量读取的时候不得不到主内存中再次获取。
3. Java内存模型
Java的内存模型(Java Memory Mode, JMM) 指定了Java虚拟机如何与计算机的主存(RAM) 进行工作, 如图所示, 理解Java内存模型对于编写行为正确的并发程序是非常重要的。在JDK 1.5以前的版本中, Java内存模型存在着一定的缺陷, 在JDK 1.5的时候,JDK官方对Java内存模型重新进行了修订, JDK 1.8及最新的JDK版本都沿用了JDK 1.5修订的内存模型。
Java的内存模型决定了一个线程对共享变量的写入何时对其他线程可见, Java内存模型定义了线程和主内存之间的抽象关系,具体如下。
- 共享变量存储于主内存之中,每个线程都可以访问。
- 每个线程都有私有的工作内存或者称为本地内存。
- 工作内存只存储该线程对共享变量的副本。
- 线程不能直接操作主内存,只有先操作了工作内存之后才能写入主内存。
- 工作内存和Java内存模型一样也是一个抽象的概念, 它其实并不存在, 它涵盖了缓存、寄存器、编译器优化以及硬件等。
Java的内存模型是一个抽象的概念, 其与计算机硬件的结构并不完全一样, 比如计算机物理内存不会存在栈内存和堆内存的划分,无论是堆内存还是虚拟机栈内存都会对应到物理的主内存, 当然也有一部分堆栈内存的数据有可能会存人CPU Cache寄存器中。图所示的是Jave内存模型与CPU硬件架构的交互图。