文章目录
一、volatile关键字的两大作用
1.1、保证线程间可见性
JAVA进程中的所有线程是共享堆内存的,线程也有自己的内存,当其中一个线程修改了共享变量时,另一个线程的值可能没有及时更新,volatile关键字标志变量为易变的,CPU会实时监听。保证线程间可见性是基于缓存一致性协议实现的。
当volatile修饰的是引用类型时,引用类型中的变量发生变化是监测不到的,所以volatile一般用于修饰基本数据类型
1.2、防止指令重排
1.2.1、什么是指令重排?
CPU为了提高执行效率,当两条指令没有依赖关系时,执行的顺序可能会发生变化,如CPU在进行读等待时,会去执行下一条指令,前提是指令没有依赖关系。
1.2.2、volatile是如何实现防止指令重排的?
- 字节码层面:
变量的修饰符access_flags中包含了ACC_VOLATILE标识
- JVM层面(HotSpot虚拟机):
JVM屏障规范:Store、Load
StoreStoreBarrier - volatile变量的写操作 - StoreLoadBarrie
LoadLoadBarrier - volattile变量的读操作 - LoadStoreBarrier
- 操作系统层面:
CPU内存屏障
sfence:(save)在sfence指令前的写操作必须在指令后面的写操作前完成。
lfence:(load)在ifence指令前的写操作必须在指令后面的写操作前完成。
mfence:(mix)在mfence指令前的读写操作必须在指令后面的读写操作前完成。
原子指令,lock指令,执行时会锁住内存子系统来确保执行顺序
1.2.3、JVM规范中的8大happen-before原则
- 程序次序规则:在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变!
- 管程锁定规则:就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!
- volatile变量规则:就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。
- 线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。
- 线程终止规则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。
- 传递规则:这个简单的,就是happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C。
- 对象终结规则:这个也简单的,就是一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。
JAVA语言规范文档中有具体说明,jls12.pdf中的第674页有详细介绍happen-before原则,提取码:36sa
文档链接
二、证明volatile可保证线程间可见性的程序
import java.util.concurrent.TimeUnit;
/**
* @author IT00ZYQ
* @date 2021/5/22 20:50
**/
public class TestVolatile {
public /*volatile*/ boolean running = true;
public void m() {
System.out.println("m() start ...");
while (running) {
}
System.out.println("m() end ...");
}
public static void main(String[] args) {
TestVolatile t = new TestVolatile();
new Thread(t::m).start();
try {
// 主线程睡一秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 此时在主线程修改running为false
// 如果running没有加volatile,此时虽然修改了running
// 但是由于每个线程会有自己的缓存
// 没有及时更新,导致running变量的修改对其他线程并不可见
// volatile可保证线程间可见性
t.running = false;
}
}
三、证明指令重排序存在的程序
/**
* @author IT00ZYQ
* @Date 2021/3/9 23:17
**/
public class T10_InstructionRearrangement {
public static int a = 0, b = 0, x = 0, y = 0;
public static void main(String[] args) throws InterruptedException {
int count = 0;
while(true) {
a = 0;
b = 0;
x = 0;
y = 0;
Thread thread1 = new Thread(() -> {
a = 1;
x = b;
});
Thread thread2 = new Thread(() -> {
b = 1;
y = a;
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
// 如果没有指令重排序,可能的情况有
// x = 1 y = 1
// x = 0 y = 1
// x = 1 y = 0
// 当出现了x=0,y=0说明发生了指令重排
count ++;
if (x == 0 && y == 0){
System.out.println("运行次数:" + count);
System.out.printf("x = %d, y = %d\n", x, y);
break;
}
}
}
}
运行结果: