Java 中的volitle 关键字

http://blog.csdn.net/liujinwei2005/article/details/6295666

原帖:http://rwl6813021.javaeye.com/blog/349169

研究ThreadPoolExecutor的时候,发现其中大量使用了volatile变量。不知为何,因此做了一番查找,研究: 其中借鉴了很多网上资料。 在了解volatile变量作用前,先需要明白一些概念: 

什么是原子操作? 
所谓原子操作,就是"不可中断的一个或一系列操作" , 在确认一个操作是原子的情况下,多线程环境里面,我们可以避免仅仅为保护这个操作在外围加上性能昂贵的锁,甚至借助于原子操作,我们可以实现互斥锁。 很多操作系统都为int类型提供了+-赋值的原子操作版本,比如 NT 提供了 InterlockedExchange 等API, Linux/UNIX也提供了atomic_set 等函数。 

关于java中的原子性? 
原子性可以应用于除long和double之外的所有基本类型之上的“简单操作”。对于读取和写入出long double之外的基本类型变量这样的操作,可以保证它们会被当作不可分(原子)的操作来操作。 因为JVM的版本和其它的问题,其它的很多操作就不好说了,比如说++操作在C++中是原子操作,但在Java中就不好说了。 另外,Java提供了AtomicInteger等原子类。再就是用原子性来控制并发比较麻烦,也容易出问题。 

volatile原理是什么? 
Java中volatile关键字原义是“不稳定、变化”的意思 
使用volatile和不使用volatile的区别在于JVM内存主存和线程工作内存的同步之上。volatile保证变量在线程工作内存和主存之间一致。 
其实是告诉处理器, 不要将我放入工作内存, 请直接在主存操作我. 
实现原理:

Volatile的实现原理

那么Volatile是如何来保证可见性的呢?在x86处理器下通过工具获取JIT编译器生成的汇编指令来看看对Volatile进行写操作CPU会做什么事情。

Java代码: instance = new Singleton();//instance是volatile变量
汇编代码: 0x01a3de1d: movb $0x0,0x1104800(%esi);0x01a3de24: lock addl $0x0,(%esp);

有volatile变量修饰的共享变量进行写操作的时候会多第二行汇编代码,通过查IA-32架构软件开发者手册可知,lock前缀的指令在多核处理器下会引发了两件事情。

  • 将当前处理器缓存行的数据会写回到系统内存。
  • 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。

处理器为了提高处理速度,不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完之后不知道何时会写到内存,如果对声明了Volatile变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。

这两件事情在IA-32软件开发者架构手册的第三册的多处理器管理章节(第八章)中有详细阐述。

Lock前缀指令会引起处理器缓存回写到内存。Lock前缀指令导致在执行指令期间,声言处理器的 LOCK# 信号。在多处理器环境中,LOCK# 信号确保在声言该信号期间,处理器可以独占使用任何共享内存。(因为它会锁住总线,导致其他CPU不能访问总线,不能访问总线就意味着不能访问系统内存),但是在最近的处理器里,LOCK#信号一般不锁总线,而是锁缓存,毕竟锁总线开销比较大。在8.1.4章节有详细说明锁定操作对处理器缓存的影响,对于Intel486和Pentium处理器,在锁操作时,总是在总线上声言LOCK#信号。但在P6和最近的处理器中,如果访问的内存区域已经缓存在处理器内部,则不会声言LOCK#信号。相反地,它会锁定这块内存区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,此操作被称为“缓存锁定”,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据

一个处理器的缓存回写到内存会导致其他处理器的缓存无效。IA-32处理器和Intel 64处理器使用MESI(修改,独占,共享,无效)控制协议去维护内部缓存和其他处理器缓存的一致性。在多核处理器系统中进行操作的时候,IA-32 和Intel 64处理器能嗅探其他处理器访问系统内存和它们的内部缓存。它们使用嗅探技术保证它的内部缓存,系统内存和其他处理器的缓存的数据在总线上保持一致。例如在Pentium和P6 family处理器中,如果通过嗅探一个处理器来检测其他处理器打算写内存地址,而这个地址当前处理共享状态,那么正在嗅探的处理器将无效它的缓存行,在下次访问相同内存地址时,强制执行缓存行填充。


接下来是测试 :(通过测试能更好的发现和分析问题) 
申明了几种整形的变量,开启100个线程同时对这些变量进行++操作,发现结果差异很大: 
>>Execute End: 
>>Atomic: 100000 
>>VInteger: 38790 
>>Integer: 68749 
>>Source i: 99205 
>>Source Vi: 99286 
也就是说除了Atomic,其他的都是错误的。 

我们通过一些疑问,来解释一下。 

1:为什么会产生错误的数据? 
多线程引起的,因为对于多线程同时操作一个整型变量在大并发操作的情况下无法做到同步,而Atom提供了很多针对此类线程安全问题的解决方案,因此解决了同时读写操作的问题。


2:为什么会造成同步问题? 
Java多线程在对变量进行操作的时候,实际上是每个线程会单独分配一个针对i值的拷贝(独立内存区域),但是申明的i值确是在主内存区域中,当对i值修改完毕后,线程会将自己内存区域块中的i值拷贝到主内存区域中,因此有可能每个线程拿到的i值是不一样的,从而出现了同步问题。


3:为什么使用volatile修饰integer变量后,还是不行? 
因为volatile仅仅只是解决了存储的问题,即i值只是保留在了一个内存区域中,但是i++这个操作,涉及到获取i值、修改i值、存储i值(i=i+1),这里的volatile只是解决了存储i值得问题,至于获取和修改i值,确是没有做到同步。


4:既然不能做到同步,那为什么还要用volatile这种修饰符? 
主要的一个原因是方便,因为只需添加一个修饰符即可,而无需做对象加锁、解锁这么麻烦的操作。但是本人不推荐使用这种机制,因为比较容易出问题(脏数据),而且也保证不了同步。


5:那到底如何解决这样的问题? 
        第一种:采用同步synchronized解决,这样虽然解决了问题,但是也降低了系统的性能。 
        第二种:采用原子性数据Atomic变量,这是从JDK1.5开始才存在的针对原子性的解决方案,这种方案也是目前比较好的解决方案了。


6:Atomic的实现基本原理? 
首先Atomic中的变量是申明为了volatile变量的,这样就保证的变量的存储和读取是一致的,都是来自同一个内存块,然后Atomic提供了getAndIncrement方法,该方法对变量的++操作进行了封装,并提供了compareAndSet方法,来完成对单个变量的加锁和解锁操作,方法中用到了一个UnSafe的对象,现在还不知道这个UnSafe的工作原理(似乎没有公开源代码)。Atomic虽然解决了同步的问题,但是性能上面还是会有所损失,不过影响不大,网上有针对这方面的测试,大概50million的操作对比是250ms : 850ms,对于大部分的高性能应用,应该还是够的了。

package qflag.ucstar.test.thread;   

  

import java.util.concurrent.atomic.AtomicInteger;    
   
/** 
* 测试原子性的同步 
* @author polarbear 2009-3-14 

*/   
public class TestAtomic {    
        
    public static AtomicInteger astom_i = new AtomicInteger();    
        
    public static volatile Integer v_integer_i = 0;    
        
    public static volatile int v_i = 0;    
        
    public static Integer integer_i = 0;    
        
    public static int i = 0;    
        
    public static int endThread = 0;    
        
    public static void main(String[] args) {    
        new TestAtomic().testAtomic();    
     }    
        
    public void testAtomic() {    
            
        for(int i=0; i<100; i++) {    
            new Thread(new IntegerTestThread()).start();    
         }    
            
        try {    
            for(;;) {    
                 Thread.sleep(500);    
                if(TestAtomic.endThread == 100) {    
                     System.out.println(">>Execute End:");    
                     System.out.println(">>Atomic: /t"+TestAtomic.astom_i);    
                     System.out.println(">>VInteger: /t"+TestAtomic.v_integer_i);    
                     System.out.println(">>Integer: /t"+TestAtomic.integer_i);    
                     System.out.println(">>Source i: /t"+TestAtomic.i);    
                     System.out.println(">>Source Vi: /t"+TestAtomic.v_i);    
                    break;    
                 }    
             }    
                
         } catch (Exception e) {    
             e.printStackTrace();    
         }    
     }    
        
}    
class IntegerTestThread implements Runnable {    
    public void run() {    
        int x = 0;    
        while(x<1000) {    
             TestAtomic.astom_i.incrementAndGet();    
             TestAtomic.v_integer_i++;    
             TestAtomic.integer_i++;    
             TestAtomic.i++;    
             TestAtomic.v_i++;    
             x++;    
         }    
         ++TestAtomic.endThread;    //貌似很无敌!难道是原子性的吗? 
     }    
}

-----------------------------------------xx-----------------------------------xx-----------------------------------------------

本人继续补充:

除了TestAtomic.endThread,其他的变量都被忽略了。具体解释可参见注释。



import java.util.concurrent.atomic.AtomicInteger; 
import java.io.*;

/** 
* 测试原子性的同步 
* @author pyc 2009-3-29 

*/ 
public class TestAtomic {    
    public static final int N=10;
    public static final int M=10000;
    public static int perfect_result=M*N;
    public static int endThread = 0; 
    
private PrintWriter out;//将信息输入至文本"out.txt",因为控制台buffer可能不够.

public TestAtomic() throws IOException
{
   out =new PrintWriter(
     new BufferedWriter(
       new FileWriter("out.txt")));

       
    public static void main(String[] args) { 
         try{ 
         new TestAtomic().testAtomic(); 
         }catch(Exception e){
         System.out.println(e.getMessage());
          }
         System.out.println("OK./nStatistical report:");
         System.out.println("Covered by "+(perfect_result-endThread)+" times.");
    }   
       
    public void testAtomic() { 
        Thread[] td=new Thread[N];
        for(int i=0; i<N; i++) {   
        td[i]=new Thread(new IntegerTestThread(i+1));
        }   
        for(int i=0; i<N; i++) {   
        td[i].start();
        out.println((i+1)+" go..") ; //此处如果run()方法代码少,立即可观察到complete完成信息。
        }     
        try { 
        long temp=0; //存放了上次的endTread值。
        int count=1000; //如果temp值超过一千次的重复就可以认为结束程序。
        for(;;) { 
           //Thread.sleep(1); //有可能main线程运行过快,可以调节采样的频率。
           if(TestAtomic.endThread == perfect_result) {   
            out.println("==============/r/nPerfect!/r/n=============="); //完美匹配!
                break; 
            }
            if(temp==TestAtomic.endThread){
               out.println("Equal!!");//有重复,有可能是所有线程运行结束时的重复,也有可能是main线程采样过快。
               count--;//倒计时中。。。
            }
            else {
               temp=TestAtomic.endThread;//给temp赋新值。
               count=1000;//重新设置倒计时。
            }
            out.println("endThread = "+TestAtomic.endThread);//在此处有几率可观察当前的endThread值比上次要少。
            //这是关键之处!
            if(count<=0)
            {
               out.println("/r/nI'll be crazy if I wait for that once again!/r/nFailed, OMG!+_+");
               break;
            }
        }   
        out.close();     
        }catch(Exception e) {   
            e.printStackTrace();   
        }   
    }   
    
    class IntegerTestThread implements Runnable { 
    private int id;
    public IntegerTestThread(int i){
       this.id=i;
    }
    public void run() {   
       int i=M;//充分保证线程重叠运行
       while(i>0){
        try{
         //Thread.sleep((int)(10*Math.random()));//设置睡眠时间,从而尽可能使线程重叠运行。
        }catch(Exception e){
        ++TestAtomic.endThread;//测试该语句的“原子”性。其实做完实验,我们知道,
++i,i++,   i=i+1一样都不能保证原子性。
        //我们可以从最终的endThread值是不是等于M*N得知。
        i--;
       }   
       out.println("************/r/n"+id+" has Completed!/r/n************/r/n") ;
    }   
    } 
}

原文地址:https://www.cnblogs.com/prctice/p/4434968.html