Volatile关键字
Volatile是java虚拟机提供的轻量级的同步机制。
1、保证可见性
可见性:举个例子,有三个线程A、B、C,假设A线程想要修改主内存中的一个数据num,因为每个线程都有自己的工作内存,想要修改数据的话,需要将num获得放到自己的工作内存,然后修改完成再返回给主内存。num在修改之前等于5,A线程修改之后变为8,当B线程或C线程再去拿num数据时,获得的是8而不是5,这就是保证了可见性。总结一句话就是,某一线程修改数据后,要立马通知其他线程自己修改了!
public class JMMDemo {
private volatile static int num = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (num==0){
}
}).start();
TimeUnit.SECONDS.sleep(3);
num = 1;
System.out.println(num);
}
}
当不加volatile时,运行结果:
3秒之后,main线程将num改为1,然而另一线程并不知道,所以进入死循环。
加了volatile时,运行结果:
3秒之后,main线程将num改为1,因为volatile保证了原子性,通知了另一线程自己做了修改,所以程序正常结束。
2、不保证原子性
原子性:指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。举个例子,你跟朋友在打篮球,期间总有场上的人手机响,搞得你们每次打起来了又得等他接电话,整场下来,就他事儿多,接了10多个电话,这就搞得你们这场篮球打的贼不爽,心情极差,一场篮球没有按自己想的那样漂亮的打下来,这就破坏了原子性。再举个例子,有一个add方法,就是加一操作,多个线程去调add方法,如果有20个线程,每个线程调1000次,正常情况下最终得到的结果应该是20x1000=20000。但是线程A和线程B拿到原始数据num后,线程A快一步,先将自己加完1的数据返回给主内存,当B在加完1写回主内存的时候拿的还是原始的数据并不是线程A加完1的数据,所以就会将A之前返回的数据覆盖,这样就会造成最终的结果小于20000。直接上代码!
public class Demo {
private static int num = 0;
public static void add(){
num++;
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int j = 0; j < 2000; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" "+num);
}
}
不加volatile时,运行结果:
加上volatile时,运行结果:
从两种运行结果可以看出:volatile不能保证原子性!
怎样能够保证原子性?
方法1:可以加lock和synchronized保证原子性。
方法2:使用原子类解决原子性问题
public class Demo {
private volatile static AtomicInteger num = new AtomicInteger();
public static void add(){
num.getAndIncrement();
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int j = 0; j < 2000; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" "+num);
}
}
修改int类型为AtomicInteger类型,num.getAndIncrement()就等于num++。运行结果:
结果意料之中,使用原子类解决原子性问题!
3、防止指令重排
什么是指令重排:你写的程序,计算机并不是按照你写的那样去执行的。
volatile关键字禁止指令重排序有两层意思:
1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
举个例子:
//x、y为非volatile变量
//flag为volatile变量
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会将语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的,并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。