一、是什么?
volatile关键字是Java虚拟机提供的最轻量级的线程间同步机制。
特性:
-
可见性:当一个变量被volatile修饰时,将保证此变量对所有线程的可见性。
-
有序性:使用volatile修饰的变量能禁止指令重排优化。
一、保证可见性
在Java多线程内存模型中,线程对某个主存中的变量进行修改时,对另一个也用到这个变量的线程来说是不可见的。这样会导致多线程的情况下,会影响程序的运行结果。
例如下图代码,即使线程1修改了a,线程2中的a还是1。
public class Demo1 {
private static int a = 1;
public static void main(String[] args) {
new Thread(() -> {
while (a == 1) {}
System.out.println("a不等于1了....");
}, "线程2").start();
new Thread(() -> a = 2, "线程1").start();
}
}
private static volatile int a = 1;
使用volatile关键字,可以保证线程间对同一个资源的可见性。
二、保证有序性
2.1 重排序?
在不影响程序最终结果的情况下,JVM可修改代码的执行顺序,以改善性能。
思考一下,下面代码的打印的结果,有没有可能a和b都是1?
static int a = 0, b = 0, c = 0, d = 0;
private static Set<String> result = new HashSet<>();
public static void main(String[] args) throws InterruptedException {
while (true) {
a = 0;
b = 0;
c = 0;
d = 0;
Thread thread1 = new Thread(() -> {
a = d;
c = 1;
}, "线程1");
Thread thread2 = new Thread(() -> {
b = c;
d = 1;
}, "线程2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
result.add("a: " + a + " b: " + b);
System.out.println(result);
}
}
执行结果
是有可能的,就以线程1来说,a=d;
和c=1
交换执行顺序,是不会影响线程1的执行结果的,线程2也一样。
所以当程序执行顺序如下所示时a和b都为1。
c = 1;
d = 1;
a = d;
b = c;
编译器重排序只关注单线程的指令重排,所以volatile关键字也只能保证单线程中代码前后的有序性,因为多线程之间的代码执行顺序是没有规律的,太复杂了。
源代码到最终执行的指令序列示意图
2.2 使用
下面代码中如果不加volatile,有可能num=2
先于b = b * 2;
执行,加了volatile,就可以保证num=2
在b = b * 2;
之后执行。
但需要注意的是,volatile只能保证用到volatile变量代码前后的有序性,不保证再往后的代码(比如println之后的)。
public static volatile int num = 0;
public static void main(String[] args) {
int a = 1, b = 2;
b = b * 2;
num = 2;
System.out.println(b);
b = b * 3;
a = 2;
}
2.3 happens-before规则
编译器重排序也不是随意排序的,如果代码不符合以下规则,就可以进行重排序。
- 程序次序规则: 在一个线程中,前面的操作 Happens-Before 于后续的任意操作。
a = 1;
b = a;
a必须先于b执行,不然逻辑就变了。
-
volatile变量规则: 对一个 volatile 变量的写操作 Happens-Before 于对这个 volatile 变量的读操作。
-
传递性规则: A Happens-Before B,B Happens-Before C,那么 A Happens-Before C。
a = 1;
b = a;
c = b;
a先于b,b先于c,a就先于c。
-
锁规则: 对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
-
线程启动规则: Thread 对象的 start()方法先行发生于此线程的每一个动作。
Thread thread1 = new Thread(() -> {
a = d;
c = 1;
}, "线程1");
thread1.start();
thread1.start();
先于a = d;和c = 1;
- 线程终止规则: 和启动相似,线程中的所有操作都先行发生于对此线程的终止操作。
- 线程中断规则: 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 对象终结规则: 一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize()方法的开始。
三、不保证原子性
下面的代码,我们预想的结果应该是4000,但执行结果是无法预测的。因为num++看似是一个操作,但它包含了三个操作。
解决办法:1. 加锁 ; 2.使用AtomicInteger(其他类型使用同包下的其他类)
private static volatile int num = 0;
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
for (int i = 0; i < 2000; i++) {
num++;
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(num);
}