开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第15天,点击查看活动详情
本文主要介绍volatile的三大特性,包括可见性、不保证原子性、禁止指令重排,并举例来介绍。
17. volatile
可见性
- 不加volatile程序会死循环,线程thread一直在执行,看不见num的变化
public class JMMDemo {
//不加volatile程序会死循环,线程thread一直在执行,看不见num的变化
private volatile static int num = 0;
public static void main(String[] args) {//main
new Thread(()->{ //线程1 对主内存对变化不知道(加个vloatile)
while (num == 0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
num = 1;
System.out.println(num);
}
}
不保证原子性
- num++不是一个原子操作,具体与它的底层实现有关。对于num++,cpu是先拿到这个静态变量,然后放入累加器执行+1操作,接着将加1之后的结果放到num变量原先的内存地址中,但在线程A拿到累加器的同时,线程B也有可能拿到,在A、B的线程执行的时候,各自的累加器中的数值是不同的,就导致了num被两次更新,值却不是2。
- 针对原子性的保证,可以使用lock和synchronized。
- 还有一个JUC中的保证原子性的方法,已经在程序中写到了。
- Atomic原子类来保证原子性。AtomicInteger的加一方法,用的是底层的CAS效率极高。
public class VDemo02 {
// private volatile static int num = 0;
private volatile static AtomicInteger num = new AtomicInteger();
public static void add(){
// num++;//不是一个原子性操作
num.getAndIncrement();//这个是AtomicInteger的加一方法,用的是底层的CAS效率极高
}
public static void main(String[] args) {
//理论上num应该位20000
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
//判断线程有没有执行完
while (Thread.activeCount() > 2) { //这个2表示的是main和gc
Thread.yield();
}
//但结果不是两万,因为没有加上原子性操作。
System.out.println(Thread.currentThread().getName() + " " + num);
}
}
18. volatile禁止指令重排
计算机不是按照你“写的程序”来执行的。源代码->编译器优化重排->指令并行也可能会重排->执行
解释“上面这句话”:假设我们写了四行代码1:
int x = 1;2:int y = 2;3:x = x + 5;4:y = x * x;我们想到的执行顺序就是1234,但程序中执行顺序也肯可能是2134、2143(进行了重排)。
对于上述例子就有多种结果x = 6,y=36; x = 6,y=1。
当然处理器对于上述例子不会执行4123这样的顺序,因为处理器进行指令重排的时候是有规则的,它会考虑数据之间的依赖性! 当然这种情况可能在程序上很难遇见,但在逻辑上是很正常的。
而volatile可以避免指令重排:内存屏障(禁止上面指令和下面指令顺序交换)。CPU指令。作用:
1.保证特定的操作执行顺序!2.可以保证某些变量的内存可见性!(利用这些特性volatile实现了可见性。)
加了volatile如下: