一、什么是延迟初始化?

在Java多线程程序中,有时候需要采用延迟初始化来降低初始化类和创建对象的开销。

延迟初始化实际上就是:当我们要进行一些高开销的对象初始化操作时,只有在使用这些对象时才进行初始化。最显著的意义在于,假如程序实际上不会用到这些类,那初始化它们的开销就会被完全避免。

二、延迟初始化的错误实现方式

1、线程不安全的延迟初始化

  1. public class UnsafeLazyInitialization {
  2. private static Instance instance;
  3. public static Instance getInstance() {
  4. if (instance == null) //1:A线程执行
  5. instance = new Instance(); //2:B线程执行
  6. return instance;
  7. }
  8. static class Instance {
  9. }
  10. }

在UnsafeLazyInitialization类中,假设线程A执行代码1的同时,B线程执行代码2。此时线程A很可能看到instance引用的对象还没有完成初始化。所以我们必须对1、2这两步操作进行同步处理。

2、直接使用synchronized进行同步-有巨大的性能开销

  1. public class SafeLazyInitialization {
  2. private static Instance instance;
  3. public synchronized static Instance getInstance() {
  4. if (instance == null)
  5. instance = new Instance();
  6. return instance;
  7. }
  8. static class Instance {
  9. }
  10. }

在早期的JVM中,使用synchronized存在巨大的性能开销,这在实际的应用中时几乎不可能被接受的。

3、双重检查锁定-看似聪明的解决方案

  1. public class DoubleCheckedLocking { //1
  2. private static Instance instance; //2
  3. public static Instance getInstance() { //3
  4. if (instance == null) { //4:第一次检查
  5. synchronized (DoubleCheckedLocking.class) { //5:加锁
  6. if (instance == null) //6:第二次检查
  7. instance = new Instance(); //7:问题的根源出在这里
  8. } //8
  9. } //9
  10. return instance; //10
  11. } //11
  12. static class Instance {
  13. }
  14. }

为了克服同步带来的大量开销,人们想得到了双重检查锁定这一看似聪明的解决方案。目的是仅仅对一开始竞争状态的getInstance加锁,带来开销。

但由于代码可能的重排序,直接使用上述代码是一种错误的优化。原因如下:

示例代码第七行,即 instance = new Instance(); 可以分解为如下的三行伪代码

  1. memory = allocate();//1:分配对象的内存空间
  2. ctorInstance(memory);//2:初始化对象
  3. instance = memory;//3:设置instance指向刚分配的内存地址

假如2、3之间发生重排序,可能顺序变成如下这样

  1. memory = allocate();//1:分配对象的内存空间
  2. instance = memory;//3:设置instance指向刚分配的内存地址
  3. ctorInstance(memory);//2:初始化对象

也就是说,当instance已经指向分配的内存地址时,对象还没有被初始化。

我们再回到示例代码

  1. public class DoubleCheckedLocking { //1
  2. private static Instance instance; //2
  3. public static Instance getInstance() { //3
  4. if (instance == null) { //4:第一次检查
  5. synchronized (DoubleCheckedLocking.class) { //5:加锁
  6. if (instance == null) //6:第二次检查
  7. instance = new Instance(); //7:问题的根源出在这里
  8. } //8
  9. } //9
  10. return instance; //10
  11. } //11
  12. static class Instance {
  13. }
  14. }

当线程A在进行代码的第7行,即new Instance时,内部发生了重排序,即 instance = memory 在 ctorInstance(memory) 后进行。假如进程A刚刚进行到这两步之间。而进程B恰巧在第四行(第一次检查)处进行判断。那么线程B判断instance不为null,很可能向下进行,进而访问instance所引用的对象。而这时进程A尚未初始化instance!从而程序发生错误。

三、延迟初始化的正确实现方式

在上面的说明中了解了问题的根源后,我们可以很容易想到两个方法来实现线程安全的延迟初始化。

(1)不允许伪代码中的2和3两行重排序

(2)允许2和3重排序,但不允许其他线程"看到"这个重排序

1、基于volatile的解决方案

我们只要对之前双重检查锁定的代码进行一些小小的修改,就可以实现我们期望中的延迟初始化。

  1. public class SafeDoubleCheckedLocking {
  2. private volatile static Instance instance;
  3. public static Instance getInstance() {
  4. if (instance == null) {
  5. synchronized (SafeDoubleCheckedLocking.class) {
  6. if (instance == null)
  7. instance = new Instance();//instance为volatile
  8. }
  9. }
  10. return instance;
  11. }
  12. static class Instance {
  13. }
  14. }

当instance的引用被声明为volatile时,创建对象时的重排序就将在多线程环境中被禁止。从而实现了用双重检查锁定来实现延迟初始化。

注:这个方案需要JDK5以上版本,因为自JDK5开始使用新的JSR-133内存模型,这个规范增强了volatile的语义。

2、基于类初始化锁的解决方案

JVM在类的初始化阶段(即Class被加载后,被线程使用前),会执行类的初始化。在执行类的初始化期间,JVM会获取一个锁,这个锁可以同步多个线程对一个类初始化。基于这个特性,我们可以用以下的方式来实现延迟初始化。

注意这个锁是对于类的初始化,而非对象的!

  1. public class InstanceFactory {
  2. private static class InstanceHolder {//利用这个类的初始化锁
  3. public static Instance instance = new Instance();
  4. }
  5. public static Instance getInstance() {
  6. return InstanceHolder.instance; //这里将InstanceHolder被初始化
  7. }
  8. static class Instance {
  9. }
  10. }

当getInstance第一次被调用,发生竞争时,InstanceHolder将被初始化。其中的静态变量instance也在此时被初始化。而InstanceHolder这个类的初始化锁保证了instance的初始化是被同步的。即无论new instance时是否发生重排序,都不会被其他线程所看到。从而解决了同步问题。

类的初始化锁相关知识在此不赘述,可参考《The Art of Java Concurrency Programming》相关篇目。

四、两种解决方案的对比

我们很容易可以发现,基于类初始化锁的方案的实现代码要更加简洁。但基于volatile的双重检查锁定方案有一个额外的优势,它可以对实例字段实现延迟初始化。

当我们进行延迟初始化处理时,面对实例字段我们使用基于volatile的方案,面对静态字段我们使用集于类初始化锁的方案。

2021-1-16

Java对象延迟初始化的实现的更多相关文章

  1. java 对象的初始化流程(静态成员、静态代码块、普通代码块、构造方法)

    一.java对象初始化过程 第一步,加载该类,一个java对象在初始化前会进行类加载,在JVM中生成Class对象.加载一个类会进行如下操作,下面给出递归描述.(关于Class对象详见反射 点击这里) ...

  2. java 对象的初始化过程

    PersonDemo p=new PersonDemo("lisi",20);这句话都做了什么事情? 因为new用到了PersonDemo.class,所以会先找到PersonDe ...

  3. Java对象的初始化顺序

    new一个对象时,该对象的初始化顺序如下 : 父类中的静态成员变量 父类中的静态代码块 子类中的静态成员变量 子类中的静态代码块 父类中的非静态变量 父类中的非静态代码块 父类构造函数 子类中的非静态 ...

  4. java对象的初始化过程和创建对象的几种方式

    1.加载父类,加载父类的静态属性和静态代码块 2.加载子类,加载子类的静态属性和静态代码块 3.初始化父类中的非静态属性并赋初值,执行父类非静态代码块,执行父类构造. 4.初始化子类中的非静态属性并赋 ...

  5. JAVA对象的初始化过程

    出处:http://blog.csdn.net/andrew323/article/details/4665379 下面我们通过两个例题来说明对象的实例化过程. 例1:   编译并运行该程序会有以下输 ...

  6. Java对象的初始化

    昨天写的代码被殷老师诟病了,因为太「丑陋」. 原来我的代码结构是这样的: public class ColorRocognizer { ..... public static void main(St ...

  7. JNI_Z_08_创建Java对象

    1.步骤 : (1).获取 jclass (2).获取 构造函数的 method id (方法的名称始终为"<init>") (3).创建Java对象的两种方式: (3 ...

  8. JAVA 双重检查锁定和延迟初始化

    双重检查锁定的由来在Java程序中,有时需要推迟一些高开销的对象的初始化操作,并且只有在真正使用到这个对象的时候,才进行初始化,此时,就需要延迟初始化技术.延迟初始化的正确实现是需要一些技巧的,否则容 ...

  9. 解析Java类和对象的初始化过程

    类的初始化和对象初始化是 JVM 管理的类型生命周期中非常重要的两个环节,Google 了一遍网络,有关类装载机制的文章倒是不少,然而类初始化和对象初始化的文章并不多,特别是从字节码和 JVM 层次来 ...

随机推荐

  1. DB2版本升级(V9.7升级到V11.1)

    1.V11.1版本升级路线 DB2 11.1 可以将现有的 Db2 V9.7.Db2 V10.1 或 Db2 V10.5 实例和数据库直接升级到 Db2 V11.1.如果 Db2 服务器正在 Db2 ...

  2. saltstack 服务器批量管理

    学习saltstack 服务器批量管理 1.saltstack 简介 SaltStack是一个开源的.新的基础平台管理工具,使用Python语言开发,同时提供Rest API方便二次开发以及和其他运维 ...

  3. unity3D进阶

    前言 在之前的例子中,我们都没有用到unity的精髓,例如地形系统.物理系统.粒子系统等,本文记录unity3D的进阶简单应用 前期准备 https://unity.cn/releases/full/ ...

  4. 使用Azure Runbook 发送消息到Azure Storage Queue

    客户需要定时发送信息到Azure Storage Queue,所以尝试使用Azure Runbook实现这个需求. 首先新增一个Azure Automation Account的资源. 因为要使用Az ...

  5. STL_map和multimap容器

    一.map/multimap的简介 map是标准的关联式容器,一个map是一个键值对序列,即(key,value)对.它提供基于key的快速检索能力. map中key值是唯一的.集合中的元素按一定的顺 ...

  6. LocalDateTime去掉T

    最近在使用阿里巴巴的fastjson反序列化对象的时候,对象里面时间格式属性总是会多了一个T  2021-1-09T18:29:09.097 这个T是啥国际标准,但是我们的前端又不需要这个T,所以就要 ...

  7. proxmox ve系统绑定上联外网出口bond双网卡

    背景描述:一个客户搭建proxmox ve系统,要求上联出口双网卡绑定bond, proxmox ve下载地址:超链接 记录日期:2020/5/9 前期准备:服务器接好2个网卡 交换机:H3C 1.p ...

  8. (14)-Python3之--虚拟环境virtualenv

    1.安装virtualenv pip install virtualenv 如果是在Linux下需要把virtualenv添加到/usr/bin目录下 # find / -name virtualen ...

  9. GIT常用命令:

    1.安装好Git之后,点击鼠标右键即可看到有Git bush选项,点击即可进入Git命令行操作. 2.使用命令: git  config  --global user.name "lyh&q ...

  10. Bitter.Core系列九:Bitter ORM NETCORE ORM 全网最粗暴简单易用高性能的 NETCore 之 WITH 子句支持

    有时我们在聚合查询中,经常会有复杂的聚联查询.有时表的聚联查询SQL 子句比较复杂,DBA 会经常告诉们,能否通过WITH 子句优化.WITH 子句,是对SQL 聚联查询的优化.Bitter.Core ...