对volatile不具备原子性的理解

在阅读多线程书籍的时候,对volatile的原子性产生了疑问,问题相似于这篇文章所阐述的那样。通过一番思考给出本身的理解。
咱们知道对于可见性,Java提供了volatile关键字来保证可见性有序性但不保证原子性
普通的共享变量不能保证可见性,由于普通共享变量被修改以后,何时被写入主存是不肯定的,当其余线程去读取时,此时内存中可能仍是原来的旧值,所以没法保证可见性。html


  背景:为了提升处理速度,处理器不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存(L1,L2或其余)后再进行操做,但操做完不知道什么时候会写到内存。java

  • 若是对声明了volatile的变量进行写操做,JVM就会向处理器发送一条指令,将这个变量所在缓存行的数据写回到系统内存。可是,就算写回到内存,若是其余处理器缓存的值仍是旧的,再执行计算操做就会有问题。
  • 在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每一个处理器经过嗅探在总线上传播的数据来检查本身缓存的值是否是过时了,当处理器发现本身缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操做的时候,会从新从系统内存中把数据读处处理器缓存里。

总结下来c++

  • 第一:使用volatile关键字会强制将修改的值当即写入主存;
  • 第二:使用volatile关键字的话,当线程2进行修改时,会致使线程1的工做内存中缓存变量的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
  • 第三:因为线程1的工做内存中缓存变量的缓存行无效,因此线程1再次读取变量的值时会去主存读取。

最重要的是编程

  • 可见性:对一个volatile变量的读,老是能看到(任意线程)对这个volatile变量最后的写入。
  • 原子性:对任意单个volatile变量的读/写具备原子性,但相似于volatile++这种复合操做不具备原子性。

举2个例子,例子来源于这篇文章:缓存

例子是这样的:多线程

//线程1
boolean stop = false;
while(!stop){
    doSomething();
}
 
//线程2
stop = true;

原文:这段代码是很典型的一段代码,不少人在中断线程时可能都会采用这种标记办法。可是事实上,这段代码会彻底运行正确么?即必定会将线程中断么?不必定,也许在大多数时候,这个代码可以把线程中断,可是也有可能会致使没法中断线程(虽然这个可能性很小,可是只要一旦发生这种状况就会形成死循环了)。
  下面解释一下这段代码为什么有可能致使没法中断线程。在前面已经解释过,每一个线程在运行过程当中都有本身的工做内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在本身的工做内存当中。
  那么当线程2更改了stop变量的值以后,可是还没来得及写入主存当中,线程2转去作其余事情了,那么线程1因为不知道线程2对stop变量的更改,所以还会一直循环下去。
  可是用volatile修饰以后就变得不同了:
  第一:使用volatile关键字会强制将修改的值当即写入主存;
  第二:使用volatile关键字的话,当线程2进行修改时,会致使线程1的工做内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
  第三:因为线程1的工做内存中缓存变量stop的缓存行无效,因此线程1再次读取变量stop的值时会去主存读取。
到这里可能看起来没什么问题,咱们来看例子2并发

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根本就不会看到修改的值。app

你们是否是有这样的疑问:“线程1在读取inc为10后被阻塞了,没有进行修改因此不会去通知其余线程,此时线程2拿到的仍是10,这点能够理解。可是后来线程2修改了inc变成11后写回主内存,这下是修改了,线程1再次运行时,难道不会去主存中获取最新的值吗?按照volatile的定义,若是volatile修饰的变量发生了变化,其余线程应该去主存中拿变化后的值才对啊?”
  是否是还有:例子1中线程1先将stop=flase读取到了工做内存中,而后去执行循环操做,线程2将stop=true写入到主存后,为何线程1的工做内存中stop=false会变成无效的?.net

其实严格的说,对任意单个volatile变量的读/写具备原子性,但相似于volatile++这种复合操做不具备原子性。在《Java并发编程的艺术》中有这一段描述:“在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每一个处理器经过嗅探在总线上传播的数据来检查本身缓存的值是否是过时了,当处理器发现本身缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操做的时候,会从新从系统内存中把数据读处处理器缓存里。”咱们须要注意的是,这里的修改操做,是指的一个操做线程

  • 例子1中,由于是while语句,线程会不断读取stop的值来判断是否为false,每一次判断都是一个操做。这里是从缓存中读取。单个读取操做是具备原子性的,因此当例子1中的线程2修改了stop时,因为volatile变量的可见性,线程1再读取stop时是最新的值,为true。
  • 而例子2中,为何自增操做会出现那样的结果呢?能够知道自增操做是三个原子操做组合而成的复合操做。在一个操做中,读取了inc变量后,是不会再读取的inc的,因此它的值仍是以前读的10,它的下一步是自增操做。