java volatile关键字解析

volatile是什么

  volatile在java语言中是一个关键字,用于修饰变量。被volatile修饰的变量后,表示这个变量在不同线程中是共享,编译器与运行时都会注意到这个变量是共享的,因此不会对该变量进行重排序。上面这句话可能不好理解,但是存在两个关键,共享和重排序。

变量的共享

先来看一个被举烂了的例子:

public class VolatileTest {

    boolean isStop = false;

    public void test() {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                isStop = true;
            }
        };
        Thread t2 = new Thread() {
            @Override
            public void run() {
                while (!isStop) {
                }
            }
        };
        t2.start();
        t1.start();
    }

    public static void main(String args[]) throws InterruptedException {
        new VolatileTest().test();
    }
}

(注:线程2中,while内容里如果写个System.out.prientln(""),导致循环退出,目前没明白什么原因。)

 

  上面的代码是一种典型用法,检查某个标记(isStop)的状态判断是否退出循环。但是上面的代码有可能会结束,也可能永远不会结束。因为每一个线程都拥有自己的工作内存,当一个线程读取变量的时候,会把变量在自己内存中拷贝一份。之后访问该变量的时候都通过访问线程的工作内存,如果修改该变量,则将工作内存中的变量修改,然后再更新到主存上。这种机制让程序可以更快的运行,然而也会遇到像上述例子这样的情况。

  存在一种情况,isStop变量被分别拷贝到t1、t2两个线程中,此时isStop为false。t2开始循环,t1修改本地isStop变量称为true,并将isStop=true回写到主存,但是isStop已经在t2线程中拷贝过一份,t2循环时候读取的是t2 工作内存中的isStop变量,而这个isStop始终是false,程序死循环。我们称t2对t1更新isStop变量的行为是不可见的。

  如果isStop变量通过volatile进行修饰,t2修改isStop变量后,会立即将变量回写到主存中,并将t1里的isStop失效。t1发现自己变量失效后,会重新去主存中访问isStop变量,而此时的isStop变量已经变成true。循环退出。

  

volatile boolean isStop = false;

 

代码的重排序

再来看一个被举烂了的例子:

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2
 
//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

    (注:感觉很难模拟,我没能模拟出来,也没找到他人的模拟结果)

 

  如上代码示例,按照正常的想法,context初始化后,再把inited赋值为true。但是有可能有语句2先执行,再执行语句1的情况。导致线程2中doSomeThingWithConfig报错。因为jvm对代码进行编译的时候会进行指令优化,调整互不关联的两行代码执行顺序,在单线程的时候,指令优化会保证优化后的结果不会出错。但是在多线程的时候,可能发生像上述例子里的问题。如果上述的inited用volatile修饰,就不会有问题。

  《深入理解Java虚拟机》中有一句话:“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”,lock前缀指令生成一个内存屏障。保证重排序后的指令不会越过内存屏障,即volatile之前的代码只会在volatile之前执行,volaiter之后的代码只会在volatile之后执行。

 

  

volatile怎么用

  volatile关键字一般用于标记变量的修饰,类似上述例子。《Java并发编程实战》中说,volatile只保证可见性,而加锁机制既可以确保可见性又可以确保原子性。当且仅当满足以下条件下,才应该使用volatile变量:

1、对变量的写入操作不依赖变量的当前值,或者确保只有单个线程变更变量的值。

2、该变量不会于其他状态一起纳入不变性条件中

3、在访问变量的时候不需要加锁。

 

  逐一分析:

  第一条说明volatile不能作为多线程中的计数器,计数器的count++操作,分为三步,第一步先读取count的数值,第二步count+1,第三步将count+1的结果写入count。volatile不能保证操作的原子性。上述的三步操作中,如果有其他线程对count进行操作,就可能导致数据出错。

 

  第二条:

public class VolatileTest {


    private volatile int lower = 0;
    private volatile int upper = 5;

    public int getLower() {
        return lower;
    }

    public int getUpper() {
        return upper;
    }

    public void setLower(int lower) {
        if (lower > upper) {
            return;
        }
        this.lower = lower;
    }

    public void setUpper(int upper) {
        if (upper < lower) {
            return;
        }
        this.upper = upper;
    }
}

 

    上述程序中,lower初始为0,upper初始为5,并且upper和lower都用volatile修饰。我们期望不管怎么修改upper或者lower,都能保证upper>lower恒成立。然而如果同时有两个线程,t1调用setLower,t2调用setUpper,两线程同时执行的时候。有可能会产生upper<lower这种不期望的结果。

    测试代码:

public void test() {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                setLower(4);
            }
        };
        Thread t2 = new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                setUpper(3);
            }
        };

        t1.start();
        t2.start();

        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("(low:" + getLower() + ",upper:" + getUpper() + ")");

    }

    public static void main(String args[]) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            VolatileTest volaitil = new VolatileTest();
            volaitil.test();
        }
    }

   

      输出结果:

  

 

  此时程序一直正常运行,但是出现的结果却是我们不想要的。

 

 

  第三条:当访问一个变量需要加锁时,一般认为这个变量需要保证原子性和可见性,而volatile关键字只能保证变量的可见性,无法保证原子性。

 

 

  最后贴个volatile的常见例子,在单例模式双重检查中的使用:

  

public class Singleton {

    private static volatile Singleton instance=null;

    private Singleton(){
    }

    public static Singleton getInstance(){
        if(instance==null){
            synchronized(Singleton.class){
                if(instance==null){
                    instance=new Singleton();
                }
            }
        }
        return instance;
    }

}

 

  new Singleton()分为三步,1、分配内存空间,2、初始化对象,3、设置instance指向被分配的地址。然而指令的重新排序,可能优化指令为1、3、2的顺序。如果是单个线程访问,不会有任何问题。但是如果两个线程同时获取getInstance,其中一个线程执行完1和3步骤,此时其他的线程可以获取到instance的地址,在进行if(instance==null)时,判断出来的结果为false,导致其他线程直接获取到了一个未进行初始化的instance,这可能导致程序的出错。所以用volatile修饰instance,禁止指令的重排序,保证程序能正常运行。(Bug很难出现,没能模拟出来)。

  然而,《java并发编程实战中》中有对DCL的描述如下:"DCL的这种使用方法已经被广泛废弃了——促使该模式出现的驱动力(无竞争同步的执行速度很慢,以及JVM启动很慢)已经不复存在了,因而它不是一种高效的优化措施。延迟初始化占位类模式能带来同样的优势,并且更容易理解。",其实我个小码畜的角度来看,服务端的单例更多时候做延迟初始化并没有很大意义,延迟初始化一般用来针对高开销的操作,并且被延迟初始化的对象都是不需要马上使用到的。然而,服务端的单例在大部分的时候,被设计为单例的类大部分都会被系统很快访问到。本篇文章只是讨论volatile,并不针对设计模式进行讨论,因此后续有时间,再补上替代上述单例的写法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值