1. volatile的作用?
Volatile:volatile是Java提供的一种轻量级的同步机制
- 保证内存可见性
- 不保证原子性
- 防止指令重排序
2.验证保证内存可见性
下面来看一段代码
import java.util.concurrent.TimeUnit;
public class VolatileDemo {
private static int num=0;
public static void main(String[] args) {
new Thread(()->{
while (num==0){
}
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num=1;
System.out.println("num="+num+"-----------跳出循环");
}
}
我们先分析一下结果:
正常情况下应该是线程A一直无限循环,直到主线程执行完num=1后,线程A终止。
实际执行结果:
主线程已经执行完num=1了,但是线程A并没有终止循环。这是为什么呢?
这时需要引入一个叫JMM的概念。
3.JMM
JMM:java内存模型(java memory model)
是Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
Java内存模型中定义了以下8种操作来完成主内存与工作内存之间数据的交互,虚拟机实现时必须保证每一种操作都是原子的、不可再分的:
lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
由上图我们可以看出,线程A与主线程各自都有自己的工作内存,使得主线程修改了num 的值,线程A并没有及时可见,因此产生上面的问题(内存不可见性)
4.程序加volatile关键字后
import java.util.concurrent.TimeUnit;
public class VolatileDemo {
private static volatile int num=0;
public static void main(String[] args) {
new Thread(()->{
while (num==0){
}
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num=1;
System.out.println("num="+num+"-----------跳出循环");
}
}
执行结果:
可以看出结果如我们所愿,加了关键字volatile后(volatile变量不会被缓存在线程各自的工作内存中,直接操作主存,因此在读取volatile类型的变量时总会返回最新写入的值。)
5.验证不保证原子性。
public class VolatileDemo {
private static volatile int num=0;
public static void main(String[] args) {
for (int i=1;i<=10;i++) {
new Thread(() -> {
for (int j = 1; j <= 100; j++) {
num++;
}
}, "A").start();
}
//main,GC线程
while(Thread.activeCount()>2){
//尝试让出执行权(不保证其他线程一定会获得执行权)
Thread.yield();
}
System.out.println("num="+num);
}
}
正常来说,执行结果应当是输出num=1000。
实际结果如下:
分析:
我们对程序进行反编译之后
可以看出num++共有三部操作(并不是原子操作)。在多线程情况下执行某一部分的时候,可能会被其他线程插入,造成结果不如我们所意,因此volatile并不能保证原子性。
解决原子性操作可以使用原子类:
import java.util.concurrent.atomic.AtomicInteger;
public class VolatileDemo {
private volatile static AtomicInteger num=new AtomicInteger();
public static void main(String[] args) {
for (int i=1;i<=10;i++) {
new Thread(() -> {
for (int j = 1; j <= 100; j++) {
num.getAndIncrement();
}
}, "A").start();
}
//main,GC线程
while (Thread.activeCount()>2){
//尝试让出执行权(不保证其他线程一定会获得执行权)
Thread.yield();
}
System.out.println("num="+num);
}
}
6.验证防止指令重排序
指令重排序:简而言之就是你写的代码, 计算机并不一定按照你的来执行。
源代码–>编译器优化的重排–> 指令并行也可能会重排–> 内存系统也会重排—> 执行
int x = 1; // 1
int y = 2; // 2
x = x + 5; // 3
y = x * x; // 4
我们所期望的:1234 但是可能执行的时候回变成 2134 1324,这对结果并没影响。
不可能是 4123!
但是可能会出现以下影响:
假设a=0,b=0
线程A | 线程B |
---|---|
x=a | y=b |
b=1 | a=2 |
正常结果:
x=0;y=0;
但是如果出现指令重排:
线程A | 线程B |
---|---|
b=1 | a=2 |
x=a | y=b |
出现多线程诡异的结果x=2,b=1
volatile防止指令重排原理:加了volatile会设有内存屏障。
在极限并发的情况下,单例懒汉式也有会指令重排的现象,之后会专门写一篇博客。