前言
从Java内存模型出发,结合并发编程中的原子性、可见性、有序性三个角度分析volatile所起的作用,并从汇编角度大致说了volatile的原理,说明了该关键字的应用场景;在这补充一点,分析下volatile是怎么在单例模式中避免双检锁出现的问题的。
 
并发编程的3个条件
1、原子性:要实现原子性方式较多,可用synchronized、lock加锁,AtomicInteger等,但volatile关键字是无法保证原子性的;
2、可见性:要实现可见性,也可用synchronized、lock,volatile关键字可用来保证可见性;
3、有序性:要避免指令重排序,synchronized、lock作用的代码块自然是有序执行的,volatile关键字有效的禁止了指令重排序,实现了程序执行的有序性;
 
双重检查锁定模式
双重检查锁定(Double check locked)模式经常会出现在一些框架源码中,目的是为了延迟初始化变量。这个模式还可以用来创建单例。下面来看一个 Spring 中双重检查锁定的例子。
这个例子中需要将配置文件加载到 handlerMappings中,由于读取资源比较耗时,所以将动作放到真正需要 handlerMappings的时候。我们可以看到 handlerMappings前面使用了volatile。有没有想过为什么一定需要 volatile?虽然之前了解了双重检查锁定模式的原理,但是却忽略变量使用了 volatile。
下面我们就来看下这背后的原因。
 
错误的延迟初始化例子
想到延迟初始化一个变量,最简单的例子就是取出变量进行判断。
这个例子在单线程环境可以正常运行,但是在多线程环境就有可能会抛出空指针异常。为了防止这种情况,我们需要在该方法上使用 synchronized。这样该方法在多线程环境就是安全的,但是这么做就会导致每次方法调用都需要获取与释放锁,开销很大。
深入分析可以得知只有在初始化的变量的需要真正加锁,一旦初始化之后,直接返回对象即可。
所以我们可以将该方法改造以下的样子。
这个方法首先判断变量是否被初始化,没有被初始化,再去获取锁。获取锁之后,再次判断变量是否被初始化。第二次判断目的在于有可能其他线程获取过锁,已经初始化改变量。第二次检查还未通过,才会真正初始化变量。
这个方法检查判定两次,并使用锁,所以形象称为双重检查锁定模式。
这个方案缩小锁的范围,减少锁的开销,看起来很完美。然而这个方案有一些问题却很容易被忽略。
 
new 实例背后的指令
这个被忽略的问题在于 Cache cache=new Cache()这行代码并不是一个原子指令。使用 javap -c指令,可以快速查看字节码。
从字节码可以看到创建一个对象实例,可以分为三步:
分配对象内存
调用构造器方法,执行初始化
将对象引用赋值给变量。
虚拟机实际运行时,以上指令可能发生重排序。以上代码 2,3 可能发生重排序,但是并不会重排序 1 的顺序。也就是说 1 这个指令都需要先执行,因为 2,3 指令需要依托 1 指令执行结果。
Java 语言规规定了线程执行程序时需要遵守 intra-thread semantics。**intra-thread semantics ** 保证重排序不会改变单线程内的程序执行结果。这个重排序在没有改变单线程程序的执行结果的前提下,可以提高程序的执行性能。
虽然重排序并不影响单线程内的执行结果,但是在多线程的环境就带来一些问题。
上面错误双重检查锁定的示例代码中,如果线程 1 获取到锁进入创建对象实例,这个时候发生了指令重排序。当线程1 执行到 t3 时刻,线程 2 刚好进入,由于此时对象已经不为 Null,所以线程 2 可以自由访问该对象。然后该对象还未初始化,所以线程 2 访问时将会发生异常。
 
volatile 作用
正确的双重检查锁定模式需要需要使用 volatile。volatile主要包含两个功能。
保证可见性。使用 volatile定义的变量,将会保证对所有线程的可见性。
禁止指令重排序优化。
由于 volatile禁止对象创建时指令之间重排序,所以其他线程不会访问到一个未初始化的对象,从而保证安全性。
注意,volatile禁止指令重排序在 JDK 5 之后才被修复
 
使用局部变量优化性能
重新查看 Spring 中双重检查锁定代码。
可以看到方法内部使用局部变量,首先将实例变量值赋值给该局部变量,然后再进行判断。最后内容先写入局部变量,然后再将局部变量赋值给实例变量。
使用局部变量相对于不使用局部变量,可以提高性能。主要是由于 volatile变量创建对象时需要禁止指令重排序,这就需要一些额外的操作。
总结
对象的创建可能发生指令的重排序,使用 volatile可以禁止指令的重排序,保证多线程环境内的系统安全。

双重检查锁单例模式为什么要用volatile关键字?的更多相关文章

  1. Java中的双重检查锁(double checked locking)

    最初的代码 在最近的项目中,写出了这样的一段代码 private static SomeClass instance; public SomeClass getInstance() { if (nul ...

  2. 单例模式中用volatile和synchronized来满足双重检查锁机制

    背景:我们在实现单例模式的时候往往会忽略掉多线程的情况,就是写的代码在单线程的情况下是没问题的,但是一碰到多个线程的时候,由于代码没写好,就会引发很多问题,而且这些问题都是很隐蔽和很难排查的. 例子1 ...

  3. 对象部分初始化:原理以及验证代码(双重检查锁与volatile相关)

    对象部分初始化:原理以及验证代码(双重检查锁与volatile相关) 对象部分初始化被称为 Partially initialized objects / Partially constructed ...

  4. 为什么双重检查锁模式需要 volatile ?

    双重检查锁定(Double check locked)模式经常会出现在一些框架源码中,目的是为了延迟初始化变量.这个模式还可以用来创建单例.下面来看一个 Spring 中双重检查锁定的例子. 这个例子 ...

  5. Java基础教程:多线程杂谈——双重检查锁与Volatile

    Java基础教程:多线程杂谈——双重检查锁与Volatile 双重检查锁 有时候可能需要推迟一些高开销的对象初始化操作,并且只有在使用这些对象时才进行初始化.此时程序员可能会采用延迟初始化.但要正确实 ...

  6. 【Java学习笔记】线程安全的单例模式及双重检查锁—个人理解

    搬以前写的博客[2014-12-30 16:04] 在web应用中服务器面临的是大量的访问请求,免不了多线程程序,但是有时候,我们希望在多线程应用中的某一个类只能新建一个对象的时候,就会遇到问题. 首 ...

  7. 从简单示例看对象的创建过程, 为什么双重检查的单例模式,分析Volatile关键字不能少

    编译指令 :javac Test.java 反编译指令: javap -v Test 代码 public class ObjectTest { int m = 8; public static voi ...

  8. C++的双重检查锁并不安全(转)

    一个典型的单例模式构建对象的双重检查锁如下: static Singleton * getSingleObject() { if(singleObject==NULL) { lock(); if(si ...

  9. 双重检查锁实现单例(java)

    单例类在Java开发者中非常常用,但是它给初级开发者们造成了很多挑战.他们所面对的其中一个关键挑战是,怎样确保单例类的行为是单例?也就是说,无论任何原因,如何防止单例类有多个实例.在整个应用生命周期中 ...

随机推荐

  1. shodan 文档学习笔记

    Table of Contents 1. Introduction 1.1. All About the Data 1.2. Data Collection 1.3. SSL in Depth 1.3 ...

  2. PowerMock学习(六)之Mock Final的使用

    Mock Final mockfinal相对来说就比较简单了,使用powermock来测试使用final修饰的method或class,比较简单,接口调用部分,还是service调用dao. 对于接口 ...

  3. 在React旧项目中安装并使用TypeScript的实践

    前言 本篇文章默认您大概了解什么是TypeScript,主要讲解如何在React旧项目中安装并使用TypeScript. 写这个的目的主要是网上关于TypeScript这块的讲解虽然很多,但都是一些语 ...

  4. node中mysql和短信使用方法(3)

    一.mysql的使用 使用mysql首先得有数据库并且表里面有数据,我创建了数据库newsql,里面Tables有表company等等. company有id,name,other等字段 1.导入my ...

  5. css优先级之important

    css优先级之important

  6. Nginx服务器部署 负载均衡 反向代理

    Nginx服务器部署负载均衡反向代理 LVS Nginx HAProxy的优缺点 三种负载均衡器的优缺点说明如下: LVS的优点: 1.抗负载能力强.工作在第4层仅作分发之用,没有流量的产生,这个特点 ...

  7. sql注入问题回顾

    (以下语法均为在python中使用mysql语句,部分代码省略,使用python中的pymsql模块获取游标对象即可直接执行sql语句) sql注入:在传入参数的时候做出改变,使得插入数据这条sql语 ...

  8. Java 添加Word形状或图形

    本文将介绍通过java编程在Word文档中添加形状(图形),包括添加单个图形.组合图形,以及格式化图形样式,如设置形状填充色.大小.位置.边框样式.边框颜色.边框粗细.图形旋转角度.图形文本环绕方式等 ...

  9. 程序员的算法课(11)-KMP算法

    版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/m0_37609579/article/de ...

  10. supersocket/SocketEngin/BootstrapFactory.cs 详解

    using System; using System.Collections.Generic; using System.Linq; using System.Text; using SuperSoc ...