谈谈Java中Volatile关键字的理解
volatile这个关键字可能很多朋友都听说过,或许也都用过。在java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果。在java 5之后,volatile关键字才得以重获生机。volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情。
一、前言
jmm提供了volatile变量定义、final、synchronized块来保证可见性。
用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。volatile很容易被误用,用来进行原子性操作。写了几个测试的例子,大家可以试一试。
二、主程序
public class main{ public static void main(string[] args) throws interruptedexception{ list<thread> threadlist = new arraylist<thread>(); for(int i=0; i<10; ++i){ thread thread = new thread(new runnable() { @override public void run() { single.holder.instance.add(); } }); threadlist.add(thread); thread.start(); } for(thread thread : threadlist) thread.join(); system.out.println(single.holder.instance.x); } }
三、单例模式测试
1、没有volatile,没有synchronized的情况
class single{ public int x = 0; public void add(){ try { timeunit.milliseconds.sleep(50); } catch (interruptedexception e) { e.printstacktrace(); } ++this.x; } public static class holder{ public static single instance = new single(); } }
输出结果:8, 9, 10都出现过。可以多运行,多试一试,就会发现不同的结果。
2、有volatile,没有synchronized
class single{ public volatile int x = 0; public void add(){ try { timeunit.milliseconds.sleep(50); } catch (interruptedexception e) { e.printstacktrace(); } ++this.x; } public static class holder{ public static single instance = new single(); } }
输出结果:最多出现的是9 和 10。
3、没有volatile,有synchronized
class single{ public int x = 0; public synchronized void add(){ try { timeunit.milliseconds.sleep(50); } catch (interruptedexception e) { e.printstacktrace(); } ++this.x; } public static class holder{ public static single instance = new single(); } }
输出结果:无论运行多少次都是10。
四、关于volatile在dcl(double check lock)中的应用
public class lazysingleton { private int somefield; private static lazysingleton instance; private lazysingleton() { this.somefield = new random().nextint(200)+1; // (1) } public static lazysingleton getinstance() { if (instance == null) { // (2) synchronized(lazysingleton.class) { // (3) if (instance == null) { // (4) instance = new lazysingleton(); // (5) } } } return instance; // (6) } public int getsomefield() { return this.somefield; // (7) } }
首先说明一下,为什么这种写法在java中是行不通的!
假设线程ⅰ是初次调用getinstance()方法,紧接着线程ⅱ也调用了getinstance()方法和getsomefield()方法,我们要说明的是线程ⅰ的语句(1)并不happen-before线程ⅱ的语句(7)。线程ⅱ在执行getinstance()方法的语句(2)时,由于对instance的访问并没有处于同步块中,因此线程ⅱ可能观察到也可能观察不到线程ⅰ在语句(5)时对instance的写入,也就是说instance的值可能为空也可能为非空。我们先假设instance的值非空,也就观察到了线程ⅰ对instance的写入,这时线程ⅱ就会执行语句(6)直接返回这个instance的值,然后对这个instance调用getsomefield()方法,该方法也是在没有任何同步情况被调用,因此整个线程ⅱ的操作都是在没有同步的情况下调用 ,这说明线程ⅰ的语句(1)和线程ⅱ的语句(7)之间并不存在happen-before关系,这就意味着线程ⅱ在执行语句(7)完全有可能观测不到线程ⅰ在语句(1)处对somefiled写入的值,这就是dcl的问题所在。很荒谬,是吧?dcl原本是为了逃避同步,它达到了这个目的,也正是因为如此,它最终受到惩罚,这样的程序存在严重的bug,虽然这种bug被发现的概率绝对比中彩票的概率还要低得多,而且是转瞬即逝,更可怕的是,即使发生了你也不会想到是dcl所引起的。
我的理解是:线程i 和线程ii 都有自己的工作存储,线程i 创建好了instance后,向内存刷新的时间是不确定的,所以线程ⅱ在执行语句(7)完全有可能观测不到线程ⅰ在语句(1)处对somefiled写入的值。
那么由于在java 5中多增加了一条happen-before规则:
•对volatile字段的写操作happen-before后续的对同一个字段的读操作。
利用这条规则我们可以将instance声明为volatile,即: private volatile static lazysingleton instance;
根据这条规则,我们可以得到,线程ⅰ的语句(5) -> 语线程ⅱ的句(2) (也就是线程),根据单线程规则,线程ⅰ的语句(1) -> 线程ⅰ的语句(5)和语线程ⅱ的句(2) -> 语线程ⅱ的句(7),再根据传递规则就有线程ⅰ的语句(1) -> 语线程ⅱ的句(7),这表示线程ⅱ能够观察到线程ⅰ在语句(1)时对somefiled的写入值,程序能够得到正确的行为。
补充:在java5之前对final字段的同步语义和其它变量没有什么区别,在java5中,final变量一旦在构造函数中设置完成(前提是在构造函数中没有泄露this引用),其它线程必定会看到在构造函数中设置的值。而dcl的问题正好在于看到对象的成员变量的默认值,因此我们可以将lazysingleton的somefield变量设置成final,这样在java5中就能够正确运行了。
以上内容是小编给大家介绍的java中volatile关键字的知识,希望对大家有所帮助!