这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战
前言
(1)CPU
缓存模型
CPU
缓存模型,图解:
只要记住:CPU
从内存中读取数据时会经过 CPU
缓存。
(2)举个栗子:来看看 volatile
主内存的数据会被加载到 CPU
本地缓存里去,CPU
后面会读写自己的缓存。
因为
CPU
缓存模型,默认情况下是有问题的,特别是多线程并发运行的时候,导致各个CPU
本地缓存,跟主内存没有同步。
举个栗子:
- 线程0:读取内存中
flag
,若读取的值与线程内的值不同,则输出 - 线程1:每个
1s
更新内存中的flag
public class Test {
static int flag = 0;
public static void main(String[] args) {
// 线程0:只读取
new Thread(() -> {
int localFlag = flag;
while(true) {
if (localFlag != flag) {
System.out.println("读取到了修改后的标志位:" + flag);
localFlag = flag;
}
}
}).start();
// 线程1:每隔 1s 写入一次
new Thread(() -> {
int localFlag = flag;
while(true) {
System.out.println("标志位被修改为了:" + ++localFlag);
flag = localFlag;
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
输出结果如下:
标志位被修改为了:1
读取到了修改后的标志位:1
标志位被修改为了:2
标志位被修改为了:3
标志位被修改为了:4
... ...
那么给 flag
加上 volatile
,会怎样?
public class Test {
static volatile int flag = 0;
// ... ...
}
输出结果如下:
标志位被修改为了:1
读取到了修改后的标志位:1
标志位被修改为了:2
读取到了修改后的标志位:2
标志位被修改为了:3
读取到了修改后的标志位:3
标志位被修改为了:4
读取到了修改后的标志位:4
... ...
咦,加上 volatile
,每次都能读到最新的值!!!
(3)MESI
协议:缓存一致性协议
MESI
协议:缓存一致性协议,就没有之前那个多线程并发的问题了。
依靠的机制是:
CPU
嗅探机制,使内存标志过期。
那底层是如何实现 MESI
的机制?
- 使用
:lock
前缀指令:内存屏障 - 读和写都会加一个内存屏障
图解,如下:
通过哪些指令?操作如下:
read
:从主存读取load
:将主存读取的值写入工作内存use
:从工作内存读取数据来计算assign
:将计算好的值重新赋值到工作内存中store
:将工作内存数据写入主存write
:将store
过去的变量值赋值给主存中的变量
(4)图解 Java
内存模型
Java
内存模型跟 CPU
内存模型类似。
只不过
Java
内存模型是标准化,屏蔽了底层不同计算机的区别。
Java
内存模型,图解如下:
(5)原子性、可见性、有序性
并发三大问题:原子性、可见性、有序性。
那么就从这三个问题,来看看 volatile
。
1)原子性:volatile
不行
volatile
不适合运用于需要保证原子性的场景
比如更新的时候需要依赖原来的值,而最典型的就是
a++
的场景,仅靠volatile
是不能保证a++
的线程安全的。
举个栗子,a++
:
// 两个线程,自增加 200000
public class DontVolatile implements Runnable {
volatile int a;
AtomicInteger realA = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Runnable r = new DontVolatile();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(((DontVolatile) r).a);
System.out.println(((DontVolatile) r).realA.get());
}
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
a++;
realA.incrementAndGet();
}
}
}
运行结果,如下:
187166
200000
可以看出,即便变量 a 被 volatile
修饰了,即便它最终一共执行了 200000 次的自加操作(这一点可以由原子类的最终值来印证),但是依然有一些自加操作失效了,所以最终它的结果是不到 200000 的,这就证明了 volatile
不能保证原子性。
2)可见性:volatile
可以
参见:举个栗子:来看看 volatile
3)有序性:volatile
可以
volatile
可以阻止指令重排序。
编译器和指令器,有时为了提高代码执行效率,会将指令重排序。
重排序之后,让 flag = true
先执行了,会导致线程2 直接跳过 while
等待,执行某段代码,结果 prepare()
方法还没执行,资源还没准备好,此时就会导致代码逻辑出现异常。
二、volatile
作用
回顾下,volatile
主要两个作用:
- 保证可见性
Happens-before
关系中对于volatile
是这样描述的:对一个volatile
变量的写操作happen-before
后面对该变量的读操作。 这就代表了如果变量被volatile
修饰,那么每次修改之后,接下来在读取这个变量的时候一定能读取到该变量最新的值。
- 禁止重排序
先介绍一下
as-if-serial
语义:不管怎么重排序,(单线程)程序的执行结果不会改变。在满足as-if-serial
语义的前提下,由于编译器或CPU
的优化,代码的实际执行顺序可能与编写的顺序是不同的,这在单线程的情况下是没问题的,但是一旦引入多线程,这种乱序就可能会导致严重的线程安全问题。用了volatile
关键字就可以在一定程度上禁止这种重排序。
volatile
使用场景
1)适用场合1:布尔标记位
一个比较典型的场景就是布尔标记位的场景: volatile boolean flag
用来控制多线程运行。
public class YesVolatile1 implements Runnable {
volatile boolean done = false;
AtomicInteger realA = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Runnable r = new YesVolatile1();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(((YesVolatile1) r).done);
System.out.println(((YesVolatile1) r).realA.get());
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
setDone();
realA.incrementAndGet();
}
}
private void setDone() {
done = true;
}
}
2)适用场合 2:作为触发器
场景:作为触发器,保证其他变量的可见性。
下面是 Brian Goetz
提供的一个经典例子:
Map configOptions;
char[] configText;
volatile boolean initialized = false;
. . .
// In thread A
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;
. . .
// In thread B
while (!initialized)
sleep();
// use configOptions