关键字volatite

来源:互联网 发布:守望先锋生涯数据出错 编辑:程序博客网 时间:2024/06/05 17:48
                                                                                     

                         内存模型
 

内存模型的相关概念:
    

大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。

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

1
i = i + 1;

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

  这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。本文我们以多核CPU为例。

  比如同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。但是事实会是这样吗?

  可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。

  最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。

  也就是说,如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。

  为了解决缓存不一致性问题,通常来说有以下2种解决方法:

  1)通过在总线加LOCK#锁的方式

  2)通过缓存一致性协议

  这2种方式都是硬件层面上提供的方式。

  在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。

  但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。

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

Java内存模型


在前面谈到了一些关于内存模型以及并发编程中可能会出现的一些问题。下面我们来看一下Java内存模型,研究一下Java内存模型为我们提供了哪些保证以及在java中提供了哪些方法和机制来让我们在进行多线程编程时能够保证程序执行的正确性。

  在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。那么Java内存模型规定了哪些东西呢,它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。注意,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。

  Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

 举个简单的例子:在java中,执行下面这个语句:

1
i  = 10;

   执行线程必须先在自己的工作线程中对变量i所在的缓存行进行赋值操作,然后再写入主存当中。而不是直接将数值10写入主存当中。

  那么Java语言 本身对 原子性、可见性以及有序性提供了哪些保证呢?



           volatile关键字介绍


  (一):(1)简介:

                            volatile 修饰的变量在多处理器开发的过程中保证啦共享变量的可见性,一个线程修改共享变量的时候 ,另一个线程可以读到共享变量的修改值java中的volatile 关键字用作java编译器和Thread的指示符,java中不会缓存此变量的值,始终从内存中读取  Java在Java内存模型(JMM)中引入了一些变化,它保证了从一个线程到另一个线程的变化的可见性,也就 是“happens-before”在一个线程中发生的内存写入的问题可能“泄漏”并被另一个线程看到。

  (2):volatile使用要点:

                          1:java中volatile关键字保证啦修饰的变量一直在主存储器读取而不是从本地的线程缓存中读取

                         2

:java中使用volatile关键字(包括long dubble)读写操作都是原子性的

                             3:在java中使用volatile关键字可以减少内存一致性错误的风险,因为java中的volatile变量的任何写入与该变量的后续读取建 立了一个happens-before的                                     关系

                             4:java中使用volatile关键字修饰的变量从来没有机会阻塞,因为她们只是进行简单的读写操作因此与 synchronized块不同因为变量永远不会持有锁或者                                        等待锁

                        5:该修饰符可以修饰变量为null的变量

                         6:java中的volitale关键字并不意味着原子,比如声明了变量i++操作并不是院子的,使操作变成原子必须要加入同步方法或者同步                                                     代码块进行独占访问



     (3):    与 synchronized的区别


                       1:  synchronized需要获取和释放监视器锁,而 volatile关键字不需要持有锁

                     2:在Java中的线程可以被阻塞以等待任何监视器在同步的情况下,而Java中的volatile关键字不是这样。

                     3:同步方法比Java中的volatile关键字影响性能。

                      4:由于Java中的volatile关键字仅同步线程内存和“主”内存之间的一个变量的值,而同步则同步线程内存                                                                    和“主”内存之间的所有变量的值,并锁定和释放监视器以进行引导。由于这个原因,Java中的 synchronized关键字很可                                        能比volatile具有更多的开销。                                

                       5:不能在空对象上同步,但Java中的volatile变量可以为null


当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。

而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步