volatile关键字
一、先看一个例子:统计一秒内可以进行多少次加操作
public class TestVolatile {
static boolean flag = false;
static class subThread extends Thread {
@Override
public void run() {
System.out.println("子线程开始");
int i = 0;
while (!flag) {
i++;
}
System.out.println("子线程结束 : i = " + i);
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("主线程开始执行");
new subThread().start();
subThread.sleep(1000);
flag = true;
System.out.println("主线程结束执行");
}
}
运行结果:
可以看到,尽管在打印主线程日志之前将标识位置为了true,但在主线程结束之后,子线程依然没有结束,这样的原因是什么?
再来看一个例子,子线程中是一个空循环,启动子线程之后让其sleep一秒后将num置为1,按理说应该在一秒之后程序运行结束,但实际运行时子线程却迟迟没有结束,陷入死循环;
public class TestVolatile {
private static int num = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (num == 0) {
}
}).start();
Thread.sleep(1000);
num = 1;
}
}
在上面两个代码中,在标志位前添加volatile修饰
static volatile boolean flag = false;
private volatile static int num = 0;
最后的运行结束与预期一致;
由上面的例子,引入了Java多线程中一个很重要的 volatile关键字
二、工作内存
在Java多线程中,各线程的工作内存间彼此独立,互不可见,数据指令存储在内存区域,共享的数据存储在主内存,寄存器(主存)等称之为主存;
在线程启动的时候,虚拟机会为每个线程分配一块工作内存,包含了线程内部定义的局部变量,当CPU访问时会将主内存的数据拷贝副本到工作内存,所以工作内存中也包含了线程所需要使用的共享变量(非线程内构造的对象)的副本,即为了提高执行效率;
所以在Java内存模型下,线程可以把变量保存在工作内存中,而不是直接在主内存中进行读写,当一个线程改变了一个变量的值,还未来得及将新的值写回主内存,其他线程不知道该变量的值已经做出了修改,而继续使用工作内存中的副本,造成了数据的不一致,这就是造成上面举例当中运行异常的原因;
三、volatile
volatile关键字:轻量级的线程同步关键字
1. 定义
volatile用来保证线程间变量的可见性,即数据的同步,简单的说就是当线程A对变量X进行了修改之后,在线程A后执行的其他线程能看到变量X在变动,更详细的说是要符合以下两个规则:
- 线程对变量进行修改之后,要立即写回主内存
- 线程对变量读取的时候,要从主内存中读,而不是缓存
2. volatile关键字作用
-
写过程:
1)将修改后的内容写到cache(工作内存)中,当前变量是volatile修饰的变量则会立即写回主内存;
2)其他线程的工作内存检测总线(一致性协议)上变量有被修改,则将工作内存的当前变量置为无效; -
读过程:
1)其他线程在读取时,在自己工作内存中先检测该变量是否有效,有效(当前变量其他线程未作修改)时则直接使用工作内存中内容;
2)无效(其他线程对该变量做了修改)时则直接到主内存读取最新的内容到工作内存;
3. volatile特征
- 可见性
- 有序性
要注意的是:volatile不具有原子性
-
可见性:
对于线程共享变量,当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即获取到修改后的值。- 在修改变量的时候:
1)将修改变量的副本写入主内存;
2)其它线程的副本置为无效; - 在读取变量的时候:
1)先判断volatile关键字修饰的变量副本是否有效,有效直接读取;
2)反之,则到主内存获取最新值;
- 在修改变量的时候:
-
有序性(禁止指令的重排序)
有序性:即程序执行的顺序按照代码的先后顺序执行;-
指令重排
原因:编译或者操作系统会对相应代码,执行进行优化以达到较高的系统性能; -
对代码分析研究:
在.java文件编程生成.class文件可以看出:- 在字节码文件中:加了volatile修饰的变量,在其flags中会有一个 ACC_VOLATILE;
- 在底层汇编语言上就是在该变量前添加#Lock前缀,Lock锁起到内存屏障作用,防止代码指令进行重排序;
-
- 原子性:
即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行;
volatile不保证原子操作 ( Java 中 volatile 型的 long 或 double 变量的读写是原子的)