前言
volatile可以理解成轻量级的 synchronized, 它在多CPU开发中保证了共享变量的“可见性”,可见性我们可以理解成是:当一个线程修改一个共享变量时,另一个线程可以读到这个修改的值。由于它不会引起线程的上下文切换和调度,所以如果对volatile使用恰当的话,它比synchronized的使用成本更低。
一、内存可见性和内存模型
1.1内存可见性
基本概念:
可见性是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到
1.2 内存模型(重点)
Java 内存模型 (JMM):Java虚拟机规范中定义了Java内存模型目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果。
线程,主内存,工作内存的交互关系如图所示:
简单来说:
- 所有变量储存在主内存。
- 每条线程拥有自己的工作内存,其中保存了主内存中线程使用到的变量的副本。
- 线程不能直接读写主内存中的变量,所有操作均在工作内存中完成。
由上述内容可知,每个线程有自己的工作内存,这些工作内存中的内容相当于同一个共享变量的 “副本”。
(1)此时修改线程1 的工作内存中的值,线程2 的工作内存不一定会及时变化,如下图所示:
(2)一旦线程1 修改了 a 的值,此时主内存不一定能及时同步。对应的线程2 的工作内存的 a 的值也不一定能及时同步。此时,这个时候代码中就容易出现问题。
。
二、volatile关键字
在上面,我们介绍了内存可见性问题,我们在写入 volatile 修饰的变量的时候
代码在写入 volatile 修饰的变量的时候;
将改变后的副本的值从工作内存刷新到主内存
代码在读取 volatile 修饰的变量的时候
从主内存中读取volatile变量的最新值到线程的工作内存中
从工作内存中读取volatile变量的副本
下面我们从一个实例来详细介绍volatile关键字
static class Counter {
public int flag = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while (counter.flag == 0) {
// do nothing
}
System.out.println("循环结束!");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("输入一个整数数:");
counter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
----------------------------------------------------
// 执行效果
// 当用户输入非0值时, t1 线程循环不会结束. (这显然是一个 bug)
此时,t1 读的是自己工作内存中的内容,当 t2 对 flag 变量进行修改,此时 t1 感知不到 flag 的变化。
但是如果给变量加上volatile关键字,代码就不会出错:
static class Counter {
public volatile int flag = 0;
}
-----------------------------------
// 执行效果
// 当用户输入非0值时, t1 线程循环能够立即结束.
三、volatile 不保证原子性
volatile 和 synchronized 有着本质的区别.,synchronized 能够保证原子性, volatile 保证的是内存可见性。
比如,我们对上面的代码进行调整:去掉 flag 的 volatile,给 t1 的循环内部加上 synchronized,并借助 counter 对象加锁。
static class Counter {
public int flag = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while (true) {
synchronized (counter) {
if (counter.flag != 0) {
break;
}
}
// do nothing
}
System.out.println("循环结束!");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("输入一个整数:");
counter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
总结
以上就是今天要讲的内容,在并发三特征的可见性中,volatile通过新值立即同步到主内存和每次使用前从主内存刷新机制保证了可见性。通过禁止指令重排序保证了有序性。无法保证原子性;
synchronized关键字通过lock和unlock操作保证了原子性,通过对一个变量unlock前,把变量同步回主内存中保证了可见性,通过一个变量在同一时刻只允许一条线程对其进行lock操作保证了有序性