Volatile个人理解

Posted Ioading

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Volatile个人理解相关的知识,希望对你有一定的参考价值。

http://www.cnblogs.com/dolphin0520/p/3920373.html

通过上面的阅读,记录以下个人理解,仅用以后自己复习。

 

volatile 

用途:java多线程

修饰变量 ->

  原子性 (与参考博客的理解不同,为个人理解,正确性待考究

    第一次阅读参考的博客的时候,没有发现疑问,这次重新看了一下,对参考博客上面 volatile保证原子性 这一段的分析产生不理解的地方,

  参考的博客认为不能保证原子性是由于不同线程在改变volatile 变量前,已经读取了目标变量的值到工作内存中(高速缓存),所以会发生覆盖,

  不理解的地方是既然修改了 volatile 变量,那么其它线程中的目标缓存行应该会无效,不应该会发生直接覆盖的情况,想了一下,可能是在A线

  程改变volatile 变量前,B线程读取了volatile 变量的值,并完成了改值,但是并没有刷新到主存中,仍在高速缓存中,然后A线程完成对volatile 

  变量的改值并刷新到主存中,此时,由于volatile的原因,B线程在接着执行下一步操作时(改值之后从高速缓存刷新到主存中),但是由于目标

  缓存行无效,所以刷新主存中的volatile 变量值到B线程的高速缓存中,再执行赋值到主存的操作,这样就等于把B线程中的改值操作覆盖掉了。

  可见性

   1.(变更后)即时更新

   2.需要时重新读取

      (需要注意:不读不写不违反volatile, 例如 线程A读取完成后,等待但不更新/ 同时,线程B读取并更新, 这时线程A对本身进行运算,仍会正常更新, 违反原子性)

  有序性

   3.限制编译器和处理器对指令进行重排序

    (以volatile 修饰的地方为分隔线, 在某种程度上分为三部分, 并保证每部分的执行顺序不被改变)

    int test = 10;

    test++;            test -> 11 

    volatile result = test--;     result -> 10

    test++;            test -> 11

    test--;            test -> 10

    如果在第三行用volatile 修饰,可以看作把代码分为三部分A(1,2行), B(3行), C(4,5行), 每部分的执行顺序不可以改变。每部分当中仍然遵循这个原则。

 

下面是转载的一些例子

  当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。举个简单的例子,比如下面的这段代码:

1
i = i + 1;

   当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。

 

  缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

  volatile能保证对变量的操作是原子性吗?

  下面看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Test {
    public volatile int inc = 0;
     
    public void increase() {
        inc++;
    }
     
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
         
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

   大家想一下这段程序的输出结果是多少?也许有些朋友认为是10000。但是事实上运行它会发现每次运行结果都不一致,都是一个小于10000的数字。

  可能有的朋友就会有疑问,不对啊,上面是对变量inc进行自增操作,由于volatile保证了可见性,那么在每个线程中对inc自增完之后,在其他线程中都能看到修改后的值啊,所以有10个线程分别进行了1000次操作,那么最终inc的值应该是1000*10=10000。

  这里面就有一个误区了,volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。

  在前面已经提到过,自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:

  假如某个时刻变量inc的值为10,

  线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;

  然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。

  然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。

  那么两个线程分别进行了一次自增操作后,inc只增加了1。

  解释到这里,可能有朋友会有疑问,不对啊,前面不是保证一个变量在修改volatile变量时,会让缓存行无效吗?然后其他线程去读就会读到新的值,对,这个没错。这个就是上面的happens-before规则中的volatile变量规则,但是要注意,线程1对变量进行读取操作之后,被阻塞了的话,并没有对inc值进行修改。然后虽然volatile能保证线程2对变量inc的值读取是从内存中读取的,但是线程1没有进行修改,所以线程2根本就不会看到修改的值。

  根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。

 

    

以上是关于Volatile个人理解的主要内容,如果未能解决你的问题,请参考以下文章

全面理解Java内存模型(JMM)及volatile关键字

深入理解Java内存模型——volatile

深入理解Java内存模型——volatile

java面试-谈谈你对volatile的理解

深入理解Java内存模型——volatile

volatile理解了吗?