1. 前言

Java的单例有多种实现方式:单线程下的简单版本、无法在指令重排序下正常工作的Double-Check、static、内部类+static、枚举……。这篇文章要讨论的,是在使用static实现饿汉模式的单例时,会有隐患存在。

2. Static单例的隐患

2.1 传统写法

static实现单例的代码如下:

public class Singleton {
    private static Singleton instance = new Singleton();

    private Singleton(){}

    public static Singleton getInstance(){
        return instance;
    }
}

私有的类变量、私有构造函数,再配合一个工厂方法返回实例。

2.2 隐患

乍看之下没有问题,但请考虑这样一种情况:当构造函数的执行依赖于静态变量时。代码如下:

public class Singleton {
    private static Singleton instance = new Singleton();
    private static int i = 1;      //1

    public static Singleton getInstance(){
        return instance;
    }

    private int count;

    private Singleton(){
        count = i;          //2
    }
    public int getCount(){
        return count;
    }
}

这段代码中,成员变量count的初始值是在构造函数中赋予的,与静态变量i有关。稍微有Java基础的同学都知道,静态变量是在类加载的过程中被初始化,而成员变量是在类被实例化的时候才初始化。所以,Singleton.getInstance().getCount()得到的应该是1,这样才符合逻辑。但是,事实往往是反逻辑的:运行结果是0。

2.3 问题所在

这是因为类构造函数clinit导致的。类加载过程分为多个阶段(后面会有介绍),其中有一个叫初始化阶段。在这个阶段内,会执行程序员定义好的一系列操作,这些操作都会被放入clinit方法。这些操作包括:static变量的赋值(有例外,后面会介绍)、static块的代码。它们在clinit中的组织顺序就是在源码中出现的顺序。这怎么理解呢?看例子:

public class Test{
    private static int i = 1;    //1
    static{                      //2
        i = 2;
    }
}

分析字节码,可以看到clinit方法的执行顺序是: 
 
如果1、2互换的话:

public class Test {
    static{
        i = 2;
    }
    private static int i = 1;
}

字节码变为: 
 
回到我们讨论的问题。当我们查看Singletonclinit时,发现: 

在给i赋值之前,就调用了Singleton的构造方法,而在Singleton的构造方法中: 

i赋值给了count,但此时i还没有被赋值!

2.4 解决方案

针对这个例子,有个很简单的解决方案:将i加final修饰,变成常量。这样一来,i的赋值就从初始化阶段提前到了准备阶段(后面会有介绍)。 
但这种解决方案很有局限性。如果类加载阶段不仅仅是给i赋值呢?比如用static块做一些更为复杂的操作。此时final就无能为力了。我们要保证在这些操作执行结束前,Singleton不能被实例化,否则就可能产生意想不到的结果。

所以,我的建议是:

  • 将所有的static赋值语句与逻辑操作,均放入到一个static块中,即使是static final(后面会看到,static final也不能保证一定会在准备阶段赋值)。
  • 在static块中,instance的赋值语句要放在最后。

代码如下:

public class Singleton {
    private static Singleton instance;
    private static int i;      //1

    static {    //所有对静态变量的逻辑操作都放在一个static块中
        i=1;
        instance = new Singleton();    //instance的实例化要放在最后
    }

    public static Singleton getInstance(){
        return instance;
    }

    private int count;

    private Singleton(){
        count = i;          //2
    }
    public int getCount(){
        return count;
    }
}

3. 有趣的问题

这一节,我们介绍一个有趣的问题:即便是static final修饰的常量,也不能保证一定在构造函数前被赋值。 
要理解这个问题,首先要介绍一下JVM加载类的过程。

3.1 JVM类加载过程


图中蓝色标注部分,是与变量的值相关的阶段:

  1. 在准备阶段,静态变量会在方法区获得内存空间。此外,那些常量的赋值语句将被执行。
  2. 在初始化阶段,会执行类构造方法clinit
  3. 在使用阶段的对象实例化过程中,会执行构造函数init。这也就是我们常说的实例化了。 
    知道了类加载的过程,再回头看第2节的问题,就很明显了:在初始化阶段尚未结束时,执行了使用阶段的对象实例化的代码。

3.2 有趣的问题

在第2节提到过,如果给i加final修饰,就可以解决问题。实际上,这是将i变为常量,使i=1的执行,从初始化阶段提前到了准备阶段。但是这样会有问题,下面来看这样一个问题:static final 修饰的变量一定在准备阶段被赋值吗?我们来看一个例子:

public class Singleton {
    private static final int i = initI();

    private static int initI(){
        return 1;
    }
}

本例中,istatic final所修饰,其初始化代码应该在准备阶段被执行?来看类的clinit方法: 

i是在clinit方法中被赋值的,并不在准备阶段。实际上,不止这一种情况下不行,当对static final 变量用new赋值时,也不会在准备阶段执行。因为准备阶段只会执行:static final修饰的、且赋值是字面量的赋值语句。这体现在字节码中,就是变量的字段属性表中存在ConstantValue,看下面的代码:

public class Singleton {
    private static final int i1 = 1;
    private static final String str1 = "1";
    private static final int i2 = initI();
    private static final String str2 = new String("1");

    private static int initI(){
        return 1;
    }
}

查看字节码中每个变量的属性表: 
 
明显看到,虽然都是static final修饰,但i1str1因为赋值的是字面量,所以有ConstantValue域,会在准备阶段被赋值;而i2str2一个是方法的返回值,一个是对象实例化,所以必须在clinit方法中执行: 

4. 总结

  1. static实现单例会有隐患,所以写法上要保证:所有初始化在static块中完成;instance的初始化最后完成。
  2. 并非所有static final修饰的变量都会在准备阶段被赋值,这与所赋的值是否为字面量有关。准确的说,只有编译后属性表中有ConstantValue域的变量才会在准备阶段被赋值。

static实现单例的隐患的更多相关文章

  1. .Net Static 与单例

    Static 关键字作为修饰符可以用于类.方法和成员变量上.其含义是对于整个应用程序生命周期内,访问该修饰符修饰的对象/方法/变量都引用到同一实例(内存地址).但正因如此在多线程下会出现线程安全问题: ...

  2. 有关线程安全的探讨--final、static、单例、线程安全

    我的代码中已经多次使用了线程,然后还非常喜欢使用据说是线程不安全的静态方法,然后又看到很多地方最容易提的问题就是这个东西线程不安全   于是我不免产生了以下几个亟待解决的问题: 什么样的代码是天生线程 ...

  3. C++中模板单例的跨SO(DLL)问题:RTTI,typeid,static,单例

    (转载请注明原创于潘多拉盒子) C++的模板可以帮助我们编写适合不同类型的模板类,给代码的复用性提供了极大的方便.近来写了一个涉及单例的C++模板类,简化下来可以归结为以下的代码: template ...

  4. iOS - Swift SingleClass 单例类

    前言 单例对象能够被整个程序所操作.对于一个单例类,无论初始化单例对象多少次,也只能有一个单例对象存在,并且该对象是全局的,能够被整个系统访问到. 单例类的创建 1.1 单例类的创建 1 单例类的创建 ...

  5. 多个so中模板单例的多次实例化

    在Android打包项目时,发现登录功能不能使用了,logcat中也没发现什么问题,最后一行一行log定位到了问题.原来是一个so文件中的构造函数被初始化二次!   这个单例是通过继承模板来实现的(暂 ...

  6. koa 基础(十九)es6中的单例

    1.app.js /** * es6中的单例 * 实例化的时候,无论实例多少次,构造函数只执行一次,有利于提高性能 */ class Db { static getInstance() { /*单例* ...

  7. 算法、数据结构、与设计模式等在游戏开发中的运用 (一):单例设计(Singleton Design)

    算法.数据结构.与设计模式等在游戏开发中的运用 (一):单例设计(Singleton Design) 作者: Compasslg 李涵威 1. 什么是单例设计(Singleton Design) 在学 ...

  8. JAVA之旅(十四)——静态同步函数的锁是class对象,多线程的单例设计模式,死锁,线程中的通讯以及通讯所带来的安全隐患,等待唤醒机制

    JAVA之旅(十四)--静态同步函数的锁是class对象,多线程的单例设计模式,死锁,线程中的通讯以及通讯所带来的安全隐患,等待唤醒机制 JAVA之旅,一路有你,加油! 一.静态同步函数的锁是clas ...

  9. 0013 Java学习笔记-面向对象-static、静态变量、静态方法、静态块、单例类

    static可以修饰哪些成员 成员变量---可以修饰 构造方法---不可以 方法---可以修饰 初始化块---可以修饰 内部类(包括接口.枚举)---可以修饰 总的来说:静态成员不能访问非静态成员 静 ...

随机推荐

  1. SPI 2分频MOSI实现

    module spi_25M(input clk,input rst_n,output reg sdin,output reg sclk,output reg cs);reg [7:0]cnt;reg ...

  2. python之最强王者(11)——异常(exception)

    1.Python 异常处理 python提供了两个非常重要的功能来处理python程序在运行中出现的异常和错误.你可以使用该功能来调试python程序. 异常处理: 本站Python教程会具体介绍. ...

  3. 分布式文件系统 - FastDFS 在 CentOS 下配置安装部署

    少啰嗦,直接装 看过上一篇分布式文件系统 - FastDFS 简单了解一下的朋友应该知道,本次安装是使用目前余庆老师开源的最新 V5.05 版本,是余庆老师放在 Github 上的,和目前你能在网络上 ...

  4. java web学习总结(二十四) -------------------Servlet文件上传和下载的实现

    在Web应用系统开发中,文件上传和下载功能是非常常用的功能,今天来讲一下JavaWeb中的文件上传和下载功能的实现. 对于文件上传,浏览器在上传的过程中是将文件以流的形式提交到服务器端的,如果直接使用 ...

  5. EF 添加方式比较

    using System; using System.Collections.Generic; using DBAccess.Models; using EntityFramework.BulkIns ...

  6. 9.2.2 .net framework下的MVC 控件的封装(下)

    控件封装的部分说明 可能有人觉得应该前后端分离,我也承认这是应该的方向,我们也在考虑使用ng2等简化前端.但是,我们封装控件还是因为如下原因综合考虑的: 我们这是个框架,上面支撑了许多个应用,包含几百 ...

  7. Atitit.兼具兼容性和扩展性的配置方案attilax总结

    Atitit.兼具兼容性和扩展性的配置方案attilax总结 文件配置法1 Jdbc多数据源文件配置发1 Bat文件配置法1 改进的文件配置法(采用类似i18n技术) 推荐1 使用自动化pc_id的方 ...

  8. jQuery获取短信验证码+倒计时实现

    jQuery 短信验证码倒计时 <script type="text/javascript" charset="utf-8"> $(function ...

  9. JavaScript简单分页,兼容IE6,~3KB

    简介 兼容IE6+及现代浏览器的简单分页,支持同一页面多个分页. 使用 Browser <link rel="stylesheet" href="css/GB-pa ...

  10. 深入理解javascript选择器API系列第一篇——4种元素选择器

    × 目录 [1]id属性 [2]标签名 [3]name属性[4]all 前面的话 说到最常见的DOM应用,恐怕就要数取得特定的某个或某组元素的引用了.DOM定义了许多方式来选取元素,包括getElem ...