1. volatile关键字
1.1. volatile的作用
volatile具有可见性、有序性,其保证不了原子性。下面我们对其可见性和原子性进行说明,有序性后面再说。
原子性:volatile修饰的变量,其运算并不具备原子性,对其进行多线程操作时,是不安全的。
import java.util.concurrent.TimeUnit;
class Number {
volatile int number = 0;
}
public class AtomicTest01 {
public static void main(String[] args) throws InterruptedException {
Number myNumber = new Number();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
myNumber.number++;
}
}).start();
}
TimeUnit.SECONDS.sleep(5);
System.out.println(myNumber.number);
}
}
这段代码的运行结果并不总是10000,经常会是小于10000的结果。因为number++运算,要经历多条CPU指令,如加载number的原始值到寄存器、为原始值加1、将运算结果写会CPU缓存,在多线程的情况下,线程A可能执行到为原始值加1这条指令了,线程B可能正好在执行加载number的原始值到寄存器,这样线程B加载的就是线程A还没有进行加1的值,这样最终的计算结果就会比预想的值要小。
综上可以看出,基础类型被volatile关键字修饰后,其运算也是不具备原子性的。
可见性:出现可见性问题的原因是现在进入CPU多核时代,另外为了弥补CPU与内存之间的性能差异,在每个CPU核心都设置了多级缓存,CPU在代码的过程中,会将对一些变量的修改,先放到CPU缓存中,并不立即同步到主内存。这对于代码中的局部变量还好,在每个线程中的局部变量是互不影响的。对于共享变量,就会存在比较大问题,线程A对共享变量的修改一开始只会将修改结果同步到CPU缓存中,没有写回主内存,这样线程B在读取这个共享变量的时候,就会读取到以前的旧值。
这种情况下,可以使用volatile关键字对共享变量进行修饰,这样对共享变量进行修改时,就会立即同步到主内存中,另外其他线程在读取volatile关键字修饰的共享变量时,也会主动去主内存中去查找最新的值。
import java.util.concurrent.TimeUnit;
public class VisibleTest01 {
volatile static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = Thread.ofVirtual().name("t1_thread").start(() -> {
while (flag) {
for (int i = 0; i < 1000; i++) {
i = i + 1;
}
}
System.out.println(Thread.currentThread().getName() + " stop runnuing");
});
TimeUnit.MILLISECONDS.sleep(2000);
Thread t2 = Thread.ofVirtual().name("t2_thread").start(() -> {
flag = false;
});
t1.join();
t2.join();
}
}
上面代码中,flag如果不使用volatile修饰的话,t1线程将长时间执行while循环,因为它一直捕获不到flag已经被设置为true了。
像System.out.println()(该方法中有Lock和synchronized代码块,进入这些代码块的时候,会刷新线程的CPU缓存区域)、Thread.sleep(n)等语句,很多都有类似volatile的可见性效果,如果while循环中有类似语句,也达不到上面代码的演示效果,即使flag不用volatile修饰,t1线程也不会长时间在while循环中。