volatile关键字JAVA使用

近段时间在研究多线程问题,提及到volatile这个关键字,所以研究了一下,在高并发情况下,当Bean中存在有状态的共享变量时候,就要考虑线程安全的问题了。众所周知,cpu的运行速度是远高于主存的读写速度的,在运行过程中,为了交换数据,cpu必须频繁的进行数据的读写操作,主存读写速度慢造成了cpu运行的吞吐量减少。为了解决这一问题,现在的机器都会在添加一层高速缓存(其实不止一层,有多层).以后每次cpu进行读写操作时,都直接和高速缓存交互,之后在将高速缓存中的数据回刷到主存中。在单线程情况下这没有任何问题,但在多线程环境下这会带来一些隐患。因为这会造成每个线程更改了自己高速缓存中的数据时(即使将这个更改的数据从缓存中刷回主存),其他变量读取的仍是最开始的自己高速缓存中的数据,这就造成了数据的不可见性。为了预防数据不可见性,在硬件方面有一个缓存一致性协议协议。其中最出名的MESI协议。

 

首先我们来看一个没有被volatile修饰的变量的多线程例子:



public class Demo3 {
    public  boolean run = false;

    public static void main(String[] args){
        Demo3 d = new Demo3 ();

//线程A
        new Thread (new Runnable () {
            @Override
            public void run() {

                while (d.run==false){
                       }//0
                System.out.println(Thread.currentThread ().getName ()+"执行!");//1

            }
        }).start ();
    }
//线程B
        new Thread (new Runnable () {
            @Override
            public void run() {
                for (int i = 1; i <= 10 ; i++) {
                    try {
                        Thread.sleep (100);
                        System.out.println(Thread.currentThread ().getName ()+"休息第"+i+"次");
                    } catch (InterruptedException e) {
                        e.printStackTrace ();
                    }

                }
                d.run = true;

            }
        }).start ();


}




看上去好像没什么问题,但是运行起来后,不会执行//1代码。

这是一段用来讲解并发编程中的经典代码,例如第一个线程(A线程)在执行开始执行,由于run=false,因此线程中的while循环会一直进行,A执行一段时间后,第二个线程(B线程)开始执行到一定满足条件后,并将run设置为true,理想状态下,这时A线程会退出执行循环体,往下继续执行,大部分情况下线程A会立即结束,因为前面我们说过,现在的jvm其实已经实现了缓存一致性协议,也就是说当线程B修改了run共享变量后,系统会尽可能将线程工作内存中的变量回刷到主存中。但是线程A如果并没有做主存读写操作的情况下,就只会读取存在于高速缓存中run的值,这导致永远不知道共享变量run已经在共享主存中已经修改了值。但是程序员写出这种代码的几率还是非常小的,通常只要在代码体//0中哪怕添加System.out.println(“HELLO WORLD”)这一句代码,都会引起线程对主存的读写操作,从而把存在于高速缓存中的run值更新过来,这就不会出现上述BUG了。但是这里只是提一个醒而已。

如果我们使用volatile来修饰flag,那么每次进行修改后,线程都会立即将工作内存中修改了的变量回刷到主存中,这样,其他线程读取到的永远是最新的值。这就保证了数据的可见性。

但保证了数据的可见性,volatile可以保证对被修饰的数据的操作是原子性的吗?我们先来看下面这一段代码:

public class volatile_learn {

    private volatile int inc = 0;  //volatile保证修改一个共享变量时,立即将更改后的共享变量从工作缓存刷回主存。

    public void increase() {
        this.inc++;
    }

    public static void main(String[] args) {
        final volatile_learn sharedObject = new volatile_learn ();
        for (int i = 0; i < 10; i++) {
            new Thread () {
                public void run() {
                    for (int j = 0; j < 100; j++) {
                        sharedObject.increase ();
                    }
                }

                ;
            }.start ();
        }

        while (!(Thread.activeCount () ==2)) {

        }//如果使用Idea开发软件,在run project操作时,Thread.activeCount ()为2,debug project操作时,Thread.activeCount ()为1,eclipse开发软件,不关run还是debug,Thread.activeCount ()都为1.这个是需要注意的,否则就实现不了代码功能了。
        System.out.println (sharedObject.inc);
    }
}

按照之前我们说的,每次修改volatile修饰的变量后,其他线程都能‘看见’本次修改,那么所有的线程读取到的值就是最新值,那么执行这段代码,结果应该是1000. 然而答案并非如此,在我测试的10次中,有2次出现800-999之间,如果你电脑硬件性能越差的情况下,出现这几率就会越厉害,。其实这是因为自增操作的非原子性有关。其实,自增操作看起来很简单一步,其实一共执行了三步:

1.读取变量的初始值(如果是第一次,还要将该变量copy一份然后放入工作内存作为高速cache使用)

2.cpu进行加1操作

3.cpu将修改后的值写入工作内存。

假如现在线程A读取了inc(假设此时值为10)的值,此时线程A阻塞,线程B开始抢占cpu资源,继续读取主存中Inc的值,并copy一份放入自己的工作内存中,然后进行加1操作,写入工作内存后立即(如果不用volatile,很难保证系统什么时候会回刷主存)回刷到主存中(此时volatile值为11)。此时A线程重新进入可运行状态并获得cpu资源开始运行,由于B线程已经修改了Inc的值,所以此时A线程的工作内存中的缓存已经失效,但由于A线程在阻塞前已经执行了inc的读取操作,所以线A继续执行inc+1操作,此时执行完自增操作,写入工作内存Inc的值是11,最后回刷到主存中。因此此时主存中的最终值是11,而不是12.  正是因为这样,虽然使用volatile修饰,最终运行结果仍然不是我们所期待的。究其原因,还是因为volatile只保证了数据的可见性,并不能保证对被修饰的变量的操作的原子性。

如果真的想解决这个问题,就要使用synchronized了,但是有些时候synchronized太重了,所以就看实际情况去做不同的修改吧,但是是不是以后类的所有参数都去加上volatile这个关键字呢,如果这样的话,什么参数修改了,都去做主存的修改,就失去了cpu的高速缓存存在的意义了。

稿毕!!!!!!

猜你喜欢

转载自blog.csdn.net/liangweihua123/article/details/82908939
今日推荐