存储系统是计算机的重要组成部分之一。存储系统提供写入和读出计算机工作需要的信息(程序和数据)的能力,实现计算机的信息记忆功能。现代计算机系统中常采用寄存器、高速缓存、主存、外存的多级存储体系结构。借用一张网络图片如下:
如图:其中越顶端的越靠近CPU,存储器的速度越快、容量越小、相应的价格越高。
再往后面就是内存,内存的后面就是硬盘以及一些外接存储设备等。这些设备的存储速度:
- L1 的存取速度:4 个CPU时钟周期
- L2 的存取速度:11 个CPU时钟周期
- L3 的存取速度:39 个CPU时钟周期
- RAM内存的存取速度 :107 个CPU时钟周期
打开我们电脑的任务管理器,选择 CPU 就能查看 L1、L2、L3 缓存的大小
我们编写的 Java 代码中的多线程共享变量(就是存储在堆内存的变量:如 对象的字段、static 静态变量等都是多线程共享),数据就从内存向上,先到 L3,再到 L2,再到 L1,最后到寄存器进行 CPU 计算。由于每个存储设备效率不一样,比如寄存器已经进行了 10 次计算,L1 缓存才存取一次数据,这就会导致一个比较复杂的问题:缓存一致性问题。
达到如下情况表示缓存是一致的
如果不满足以上情况,在每次运算之后,不同的进程可能会看到不同的值,这就是缓存一致性问题。
说了大半天,终于要绕回我们 Java 了
JMM是一种抽象的概念,它规定了Java 程序中多线程并发访问共享内存的方式和规则,保证了多线程程序在不同平台上的正确性和一致性。
JMM 只是一个规范,JMM 要实现屏蔽各种硬件和操作系统的访问差异,需要依托于 JVM 。所以我们可以认为 JMM 是 JVM 的一部分。
指当一个线程修改了某一个共享变量的值,其他线程是否立即知道该变更,JMM 规定了所有的变量都在主内存中
如果一个共享变量初始值为 a = 10,有 A、B、C 三个线程并发执行,假设A、B、C 线程依次修改完成 a 变量,其操作示意图如下:
这样线程 A、B 对 a 变量的操作就被覆盖了
可见性的代码示例:
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j
public class JMMTest {
private static boolean flag = true;
public static void main(String[] args) {
new Thread(()->{
while (true){
// 如果这里添加打印,flag 也会对该线程可见
// 因为 println 方法中有 Synchronized 代码块
// System.out.println();
if(!flag){
break;
}
}
},"t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("修改前");
flag = false;
log.debug("修改后");
}
}
是指同一个操作不可打断,在多线程情况下,操作不能被其他线程锁干扰。(类比数据库的事务的原子性)
有序性保证了一个线程在执行过程中,每个操作都按照一定的顺序进行。它通过禁止指令重排和设置内存屏障来实现有序性。
指令重排是指编译器在不改变程序执行结果的前提下,对指令进行重新排列,以提高性能。也就是说我们写的代码是 1234 行,但指令重排后真正执行的顺序为:3421。但此过程遵循指令之间的依赖关系,比如第 1 行为变量定义,第二行为变量的使用,那么第二行就不能被重排为第一行。
在单线程情况下,指令重排可以提高性能,且没有问题出现,但多线程情况下可能会导致程序出现错误。所以在多线程情况下,我们会采用禁止指令重排和设置内存屏障的方法来处理这类问题。
指一个线程的写操作,对另一个线程的读操作可见,那么这两个线程之间遵循 Happens-Before 原则。
总结起来就是 Happens-Before 原则就是程序执行时,必须遵循一定的客观顺序(就是要吃饭就得先张嘴,客观的顺序,不能违背客观规律)。
如我们在 JMM 可见性中的示例,volatile 关键字可解决可见性问题,用于保证某个共享变量被某个线程操作后,其操作后的结果对其他线程立即可见(重新从主内存获取更新后的值到副本),我们在上面的 JMM 可见性中的示例就是 volatile 保证可见性的一个示例
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j
public class VolatileTest1 {
private static volatile boolean flag = true;
public static void main(String[] args) {
new Thread(()->{
while (true){
// 如果这里添加打印,flag 也会对该线程可见
// System.out.println();
if(!flag){
break;
}
}
},"t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("修改前");
flag = false;
log.debug("修改后");
}
}
volatile 关键字不能保证原子性,也就是说 volatile 无法达到替换锁(Lock、Synchronized)的目的。但如果只有一个线程写,其他线程都是读的情况下,volatile 关键字可以达到我们想要的效果。所以我们常用 volatile 来保证共享数据对其他线程可见,用锁来保证共享数据到写的一致性(原子性)。
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j
public class VolatileTest1 {
public static void main(String[] args) {
Container1 c1 = new Container1();
Container2 c2 = new Container2();
for(int i = 0; i < 10; i++) {
new Thread(() -> {
for(int j = 0; j < 1000; j++){
c1.add();
c2.add();
}
}, "t"+i).start();
}
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(c1.getNum());
System.out.println(c2.getNum());
}
}
@Getter
class Container1{
private int num;
public synchronized void add(){
num++;
}
}
@Getter
class Container2{
private volatile int num;
public void add(){
num++;
}
}
运行结果:
10000
9859 //<-- 此值不确定,但肯定比10000小(原因就是 Volatile 不能保证原子性)
class Test2Container{
private int num;
private boolean flag;
public void write(){
num = 10;
flag = true; // 如果指令重排,此指令可能在 num = 10 之前执行
}
public void read(){
if(flag){
System.out.println(num);// 如果指令重排,此处可能出现 0
}
}
}
使用 volatile 禁用指令重排
@Getter
class Test2Container{
private int num;
private volatile boolean flag;
public void write(){
num = 10;
flag = true; // Volatile 写之后加入写屏障,
// 写内存屏障会确保在此次写操作之后所有的读操作都会被更新,从而保证其他线程能够立即感知到修改的值。
}
public void read(){
// 读内存屏障会确保在此次读操作之前所有的写入操作都会被完成,从而保证读取到的值是最新的
if(flag){ // // Volatile 读之前加入读屏障
System.out.println(num);
}
}
}
读写屏障的实现机制是通过在指令之间插入内存屏障(Memory Barrier)来禁止指令重排。内存屏障是一种同步机制,它会强制让编译器和处理器按照指定的顺序执行指令。
对 Volatile 变量的写指令后会加入写屏障,写内存屏障会确保在此次写操作之后所有的读操作都会被更新,从而保证其他线程能够立即感知到修改的值。
对 Volatile 变量的读指令前会加入读屏障,读内存屏障会确保在此次读操作之前所有的写入操作都会被完成,从而保证读取到的值是最新的
其实还有一种 4 种屏障的说法,但它们的理解上晦涩难懂,不如读写屏障这样解释得清楚。如果非要较这个真,可以自行搜索搜索。