Java并发16:volatile关键字的两种用法-一次性状态标志、双重检查单例模式
2018-03-18 21:30
751 查看
[超级链接:Java并发学习系列-绪论]
volatile关键字在之前的章节中多次提及:
《Java并发11:Java内存模型、指令重排、happens-before原则》:volatile就是用Lock前缀方式的内存屏障伪类型来实现的。
《Java并发14:并发三特性-可见性定义、可见性问题与可见性保证技术》:volatile关键字通过标记内存屏障来保证变量的可见性。
本章主要就volatile关键字的两种实际用法进行说明。
volatile可以看成轻量级的synchronized,相对于synchronized:编码简单、资源开销较少,同样的实现的功能也有限。
volatile能够保证变量的可见性,但是并不能保证变量的原子性和有序性。
使用volatile的前提,不受原子性和有序性影响:变量状态完全独立于任何程序的其他状态。
一次性状态标志
一种单例模式:双重检查单例模式
编码场景:
一台咖啡机能够不停地服务:为客人制作咖啡。
这台咖啡机有一个关闭按钮,当按下关闭按钮是,咖啡机停止服务。
咖啡机:
制作咖啡与关闭咖啡机:
运行结果:
结果分析:
虽然线程第二个线程将是否关闭这个状态为置为了true,但是第一个线程并没有觉察到这种状态变化,导致咖啡机无法停止工作。
这是由于不采取任务措施的情况下,共享变量的修改对其他线程未必是可见的。
然后再次运行测试代码,运行结果如下:
1.状态标志的状态转换是原子操作。例如上面的代码中,对布尔类型进行赋值操作,在Java中是原子性操作。
2.只有一次性的状态转换。上面的代码中,状态标志位只是从false转换为true,并没有继续进行从true到false的转换等。这种转换的一次性杜绝了有序性问题的产生。
可以看到,上述两个关键点,其实就是对原子性和有序性的保证。
也就是说,如果要是用volatile进行并发编程,就需要通过其他手段来保证代码的原子性和有序性。
这种模式存在问题:
instance = new DoubleCheckSingleton(); 这个操作不是原子性的。
这个操作可以划分为:
在Heap中开辟地址,进行对象初始化:new DoubleCheckSingleton()
将Heap中初始化完成的DoubleCheckSingleton对象地址,指向Thread Stack中的对象引用instance。
这两步操作经过指令重拍之后可能是2->1的顺序,因为在单线程中1->2和2->1的执行结果时一样的。
这种指令重拍在单线程下毫无问题,但是在多线程下可能存在问题:
线程A在getInstance()方法中的执行顺序是:0->2->1->3,且当前执行到了第2步,这是instance已经 ! = null 了。
线程B进入到getInstance()方法中,在第0处检查发现instance ! = null ,所以直接执行第4步:返回instance对象。
线程B继续进行后续操作,例如执行
而这时线程A还在执行第1步的初始化工作,这时,instance应用执行的实际地址还是null值。
Updates for J2SE 5.0 (aka 1.5, Tiger)
In particular, double-check idioms work in the expected way when references are declared volatile.
也就是说:在JDK1.5及以后的版本中,通过将对象引用声明成volatile的,是可以正常使用双重检查模式的。
添加volatile的双重检查模式的代码如下:
[2] Java 中的双重检查(Double-Check)
[3] 双重检查锁失效是因为对象的初始化并非原子操作?
volatile关键字在之前的章节中多次提及:
《Java并发11:Java内存模型、指令重排、happens-before原则》:volatile就是用Lock前缀方式的内存屏障伪类型来实现的。
《Java并发14:并发三特性-可见性定义、可见性问题与可见性保证技术》:volatile关键字通过标记内存屏障来保证变量的可见性。
本章主要就volatile关键字的两种实际用法进行说明。
1.volatile概述
volatile,即易变的,在Java中标识一个变量是易变变量。volatile可以看成轻量级的synchronized,相对于synchronized:编码简单、资源开销较少,同样的实现的功能也有限。
volatile能够保证变量的可见性,但是并不能保证变量的原子性和有序性。
使用volatile的前提,不受原子性和有序性影响:变量状态完全独立于任何程序的其他状态。
2.volatile的两种用法
本文只对volatile常见的两种用法进行学习:一次性状态标志
一种单例模式:双重检查单例模式
2.1.一次性状态标志
应用场景:使用一个布尔变量来标记状态,用于指示发生了一件重要的一次性事件。例如:标志配置完成了初始化、标志某种服务关闭了服务等等。编码场景:
一台咖啡机能够不停地服务:为客人制作咖啡。
这台咖啡机有一个关闭按钮,当按下关闭按钮是,咖啡机停止服务。
2.1.1.不使用volatile关键字
状态标志位:/** * 是否关闭 */ private static boolean shutdown = false;
咖啡机:
/** * <p>Title: 咖啡机</p> * * @author 韩超 2018/3/16 13:58 */ static class CoffeeMaker { /** * 关闭关闭咖啡机 */ public static void shutdown() { shutdown = true; System.out.println("关闭了咖啡机..."); } /** * 生成开发 */ public static void makeCoffee(String name) { System.out.println("咖啡机开始为客户制作咖啡..."); while (!shutdown) ; System.out.println("咖啡机已经停止工作,不再对外提供服务!"); } }
制作咖啡与关闭咖啡机:
//开始制作咖啡 new Thread(() -> { CoffeeMaker.makeCoffee(Thread.currentThread().getName()); }).start(); Thread.sleep(100); //关掉咖啡机 new Thread(() -> { CoffeeMaker.shutdown(); }).start();
运行结果:
咖啡机开始为客户制作咖啡... 关闭了咖啡机...
结果分析:
虽然线程第二个线程将是否关闭这个状态为置为了true,但是第一个线程并没有觉察到这种状态变化,导致咖啡机无法停止工作。
这是由于不采取任务措施的情况下,共享变量的修改对其他线程未必是可见的。
2.1.2.使用volatile关键字
将上面的代码做一个很小的修改:将是否关闭这个状态用volatile关键字标记:/** * 是否关闭 */ private volatile static boolean shutdown = false;
然后再次运行测试代码,运行结果如下:
咖啡机开始为客户制作咖啡... 关闭了咖啡机...
咖啡机已经停止工作,不再对外提供服务!
2.1.3.关于一次性状态标志用法的分析
使用一次性状态标志的关键点:1.状态标志的状态转换是原子操作。例如上面的代码中,对布尔类型进行赋值操作,在Java中是原子性操作。
2.只有一次性的状态转换。上面的代码中,状态标志位只是从false转换为true,并没有继续进行从true到false的转换等。这种转换的一次性杜绝了有序性问题的产生。
可以看到,上述两个关键点,其实就是对原子性和有序性的保证。
也就是说,如果要是用volatile进行并发编程,就需要通过其他手段来保证代码的原子性和有序性。
2.2.双重检查
双重检查是一种单例模式的实现方式,关于单例模式这里不多做介绍。2.2.1.不加volatile的双重检查模式
/** * <p>双重检测单例模式--不加volatile关键字</p> * * @author hanchao 2018/3/17 19:14 **/ static class DoubleCheckSingleton { private static DoubleCheckSingleton instance = null; private DoubleCheckSingleton() { } public static synchronized DoubleCheckSingleton getInstance() { if (instance == null) {//0.null判断 synchronized (DoubleCheckSingleton.class) { if (instance == null) { //耗时较长的初始化操作 instance = new DoubleCheckSingleton();//1.初始化 2.对象地址指向引用 } } } return instance;//3.返回对象 } }
这种模式存在问题:
instance = new DoubleCheckSingleton(); 这个操作不是原子性的。
这个操作可以划分为:
在Heap中开辟地址,进行对象初始化:new DoubleCheckSingleton()
将Heap中初始化完成的DoubleCheckSingleton对象地址,指向Thread Stack中的对象引用instance。
这两步操作经过指令重拍之后可能是2->1的顺序,因为在单线程中1->2和2->1的执行结果时一样的。
这种指令重拍在单线程下毫无问题,但是在多线程下可能存在问题:
线程A在getInstance()方法中的执行顺序是:0->2->1->3,且当前执行到了第2步,这是instance已经 ! = null 了。
线程B进入到getInstance()方法中,在第0处检查发现instance ! = null ,所以直接执行第4步:返回instance对象。
线程B继续进行后续操作,例如执行
instance.getName()等操作。
而这时线程A还在执行第1步的初始化工作,这时,instance应用执行的实际地址还是null值。
instance.getName()等操作,会报
NullPointerException异常。
2.2.2.添加volatile的双重检查模式
java的内存模式在持续改进之中,在jdk5中提到:Updates for J2SE 5.0 (aka 1.5, Tiger)
In particular, double-check idioms work in the expected way when references are declared volatile.
也就是说:在JDK1.5及以后的版本中,通过将对象引用声明成volatile的,是可以正常使用双重检查模式的。
添加volatile的双重检查模式的代码如下:
/** * <p>双重检测单例模式--加volatile关键字</p> * * @author hanchao 2018/3/17 19:10 **/ static class DoubleCheckedVolatileSingleton { //注意这里是volatile的 private volatile static DoubleCheckedVolatileSingleton instance = null; public static DoubleCheckedVolatileSingleton getInstance() { if (instance == null) { synchronized (DoubleCheckedVolatileSingleton.class) { if (instance == null) { instance = new DoubleCheckedVolatileSingleton(); } } } return instance; } }
参考文献
[1] Java并发编程:volatile关键字解析[2] Java 中的双重检查(Double-Check)
[3] 双重检查锁失效是因为对象的初始化并非原子操作?
相关文章推荐
- Java并发17:synchronized关键字的两种用法-同步代码块(4)和同步方法(2)
- Java 高并发第二阶段实战---高并发设计模式,内存模型,CPU一致性协议,volatile关键字剖析
- java单例双重检查锁为什么需要加volatile关键字
- Java并发编程学习笔记 深入理解volatile关键字的作用
- java 双重检查锁定及单例模式
- 【Java学习笔记】线程安全的单例模式及双重检查锁—个人理解
- Java盲点:双重检查锁定及单例模式
- java中的双重检查模式(double-check idiom)
- java并发编程 -volatile关键字
- java并发5-volatile关键字解析
- Java并发编程:volatile关键字解析
- Java并发编程:volatile关键字解析
- Java盲点:双重检查锁定及单例模式
- Java并发编程:volatile关键字解析
- Java并发编程:volatile关键字解析
- java 单例模式(双重检查锁)
- Java盲点:双重检查锁定及单例模式
- 【Java并发编程】之十五:并发编程中实现内存可见的两种方法比较:加锁和volatile变量
- Java并发编程:volatile关键字解析
- java中volatile关键字的用法,它不能保证原子性操作