轻松了解Volatile关键字

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是可见的。

posted @ 2020-07-16 16:47  雷歌儿  阅读(141)  评论(0编辑  收藏  举报