java基础一.volatile

被volatile修改的变量进行写操作的时候,会立即刷新近主存。读的时候,直接去主存中读取。

要说volatile,先简单的说一下java的内存模型。

首先,我先去百度张图片。


java中,jvm会给每个线程分配一个独立的内存空间,用于存放本线程的局部变量等,还有一部分空间叫做共享变量副本,这个部分空间存放的是从主存中复制过来的共享变量。目的是为了加快CPU的处理速度。

在这种前提下,并发环境中就很容易出现问题。

假设:

int i=0;

i++;

i++并非是原子操作。先读取到i,然后对i进行+1操作,然后将I+1后的值赋给i。

当线程A刚刚读取到i,线程B横插一足,读取i,i+1,复制给i。这时候(在线程B眼中)i=1,此时,线程A继续执行i+1=1,赋值给i,这时候i仍然是i。而使用了volatile就不会了。这就涉及到了volatile关键字的性质。

volatile有两个性质:

1.可见性:被volatile修改的变量,多个线程修改了其值,其他线程都能立即看到修改后的值。

2.有序性:对于被volatile修饰的变量,JVM会禁止对其指令重排序。

        指令重排序是JVM为了加快运行速度,可能对程序的代码进行优化,它不保证程序执行时候的代码顺序和所书写的代码顺序是一致的,但是它保证程序执行完毕的结果和代码顺序执行的结果是一致的(结果一致,过程不同)。

           volatile有序性:表现在两个方面:

                1.程序执行的时候:被volatile修饰的变量,前面的语句都执行完毕后,才会执行本语句。同样在volatile后面的语句,只有在volatile修改的变量语句之后,才会被执行。

               2.指令优化的时候:不能将volatile修饰的变量放到某些语句之后,也不能volatile修饰的变量语句后面的放到volatile之前。

volatile并不能保证原子性:

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);
     }
}
自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。

/*******************************************Copy内容****************************************************************************/

volatile的实现原理

1.可见性

处理器为了提高处理速度,不直接和内存进行通讯,而是将系统内存的数据独到内部缓存后再进行操作,但操作完后不知什么时候会写到内存。

如果对声明了volatile变量进行写操作时,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写会到系统内存。 这一步确保了如果有其他线程对声明了volatile变量进行修改,则立即更新主内存中数据。

但这时候其他处理器的缓存还是旧的,所以在多处理器环境下,为了保证各个处理器缓存一致,每个处理会通过嗅探在总线上传播的数据来检查 自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作时,会强制重新从系统内存把数据读到处理器缓存里。 这一步确保了其他线程获得的声明了volatile变量都是从主内存中获取最新的。

2.有序性

Lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。

/***********************************************************************************************************************/

什么时候会用到volatile关键字呢?

1.对变量的写不依赖于当前值(volatile不能保证原子性的结论下)

2.该变量没有包含在具有其他变量的不变式中。

常用于:状态标记量。单例模式中的double check。

public class Demo2SingleTon {


    private volatile static Demo2SingleTon singleTon;
    
    private Demo2SingleTon(){
    
    }
    
    /**
     *  双重校验锁的方式
     *  这种方式,只是会在创建的对象的时候加锁,也就是第一次创建对象的时候加锁。
     * @return
     */
    public static Demo2SingleTon getInstance(){
        if(null!=singleTon){
            synchronized (singleTon){
                if(null!=singleTon) {
                    singleTon = new Demo2SingleTon();
                }
            }
        }
        return singleTon;
    }
}

instance = new Demo2SingleTon() 这句,实际上做了三件事:

    1.给singleTon分配内存。

    2.调用SingleTon的构造函数来初始化成员变量。

    3.将instance对象指向分配的内存空间。

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。


 2018-03-21 方小白 畅游大厦


posted @ 2018-03-21 21:16  方家小白  阅读(19)  评论(0编辑  收藏  举报