static实现单例的隐患

author author     2022-08-18     274

关键词:

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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

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

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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

这段代码中,成员变量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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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

public class Test {
    static{
        i = 2;
    }
    private static int i = 1;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

字节码变为:
技术分享
回到我们讨论的问题。当我们查看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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

本例中,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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

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

4. 总结

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

















c++单例模式总结与剖析(代码片段)

...行了简单的阐述,大量用到了C++11的特性如智能指针,magicstatic,线程锁;从头到尾理解下来,对于学习和巩固C++语言特性还是很有帮助的。本文的全部代码在g++5.4.0编译器下编译运行通过,可以在我的github仓库中找到。一、什么是... 查看详情

Meyers 的单例实现实际上是如何实现单例的

】Meyers的单例实现实际上是如何实现单例的【英文标题】:HowisMeyers\'implementationofaSingletonactuallyaSingleton【发布时间】:2013-07-1617:26:53【问题描述】:我已经阅读了很多关于单例、何时应该使用和不应该使用它们以及如何安全地实... 查看详情

单例模式(下)特殊单例的实现(代码片段)

...式。”常见的有线程的单例,进程的单例(一般默认实现),多进程的单例。 实现线程唯一的单例“进程唯一”指的是进程内唯一,进程间不唯一。类比一下,“线程唯一”指的是线程内唯一,线程间 查看详情

单例的三种实现方式

1双重加锁模式publicclassSingleton{privatevolatilestaticSingletonsingleton;publicstaticSingletongetSingleton(){if(singleton==null){synchronized(Singleton.class){if(singleton==null){singleton=newSingleton();} 查看详情

单例的销毁(代码片段)

单例的代码实现staticPerson*ple;staticdispatch_once_tpredicate;dispatch_once(&predicate,^NSLog(@"2:%ld",predicate);ple=[[Personalloc]init];);单例的底层实现原理voiddispatch_once(dispatch_once_t*va 查看详情

单例的实现方式

单例模式的实现方式:单例模式要求程序中类只有一个对象。所以我们要将他的构造函数设为private,并提供一个生成对象的静态方法。第一种实现方式:  由于第一种方式是线程不安全的。因为当在if(instace==null)这句话中... 查看详情

实现单例的四种实现方法(代码片段)

...配置相同的情况下,就没必要重复产生对象浪费内存了##实现原理,为了节省空间,结合需求让同一个类多次实例化后结果指向同一个对象#可理解为开关,一次进入后更改instance的状态+_+_+_+_+_+_+_+_+_+_+_+_+_ 查看详情

ios之深入解析单例的实现和销毁的底层原理

一、单例的概念单例设计模式确保对于一个给定的类只有一个实例存在,这个实例有一个全局唯一的访问点。它通常采用懒加载的方式在第一次用到实例的时候再去创建它。单例可以保证在程序运行过程,一个类只有一个实例,... 查看详情

单例模式几种实现方式(代码片段)

...:静态常量    特点:单例的实例被声明成static和final变量了,在第一次加载类到内存中时就会初始化,所以会创建实例本身是线程安全的publicclassSingletonprivatefinalstaticSingletoninstance=newSingleton();privateSingleton()publicstati... 查看详情

关于java中无法实现真正单例的论证

如果要用java写一个单例出来,估计对遇到博文广识者来说,写法跟茴香豆的写法数量不相上下。但很遗憾,因为java中有另外一把近乎无坚不摧的矛,摧毁了java中存在单例的可能性,这把矛就是众所周知的反射。因为反射的存在... 查看详情

单例的实现方式(代码片段)

单例的实现方式:1、基于类#encoding=utf-8classSingleton(object):def__init__(self):pass@classmethoddefinstance(cls,*args,**kwargs):ifnothasattr(Singleton,"_instance"):Singleton._instance=Singleton(*args,**kwargs)returnSingleton._instances1=Singleton.instance()s2=Singleton.instance()print(s1i... 查看详情

lua面向对象----类继承多继承单例的实现

...ls/19965877?utm_source=tuicool&utm_medium=referral lua面向对象实现: 一个类就像是一个创建对象的模具。有些面向对象语言提供了类的概念,在这些语言中每个对象都是某个特定类的实例。l 查看详情

单例模式(代码片段)

目录前言破坏单例实现单例模式前言在特定场景下,我们需要在全局使用某一个对象的同一个实例,我们就需要保证一个对象不能存在多个实例。单例模式是一种很常见的设计模式;比如Servlet在Tomcat中是单例的,SpringIOC容器管... 查看详情

如何使用 NestJS 创建充当单例的服务

...一个提供单例服务的模块。想象一下QueueService,最简单的实现是单例服务。可重现的存储库:https://github.com/colthreepv/nestjs-singletons代码墙app.mo 查看详情

objective-c和swift实现单例的几种方式

  在Swift开发中,我们对于跨类调用的变量常量,因为并没有OC中使用的全局头文件中写宏的形式,我们一般采用在类外定义全局变量/常量的形式来跨类调用。而问题在于目前写的项目需要在新添加的OC写的功能模块中调用Swift... 查看详情

hashtable和hashmap作缓存,实现的两种单例的区别

参考技术Ahashmap和hashtable区别主要是hashtable对所有的读取和修操作改加了同步锁,你的那个例子如果没有其他入口可以修改managers的情况下hashmap和hashtable是一样的,否则会有并发问题,concurrenthashmap主要是用了分段锁,并发使用... 查看详情

c++单例模式(singleton)(代码片段)

文章目录1.static实现2.计数器实现1.static实现单例模式是设计模式中最简单的设计模式,也是做容易实现的模式。所谓单例模式就是让某个类只能产生一个对象。那么在C++语言中该如何设计呢?用static关键字可以实现... 查看详情

关于 Cocoa Touch 中单例的问题

...:57【问题描述】:我对OOP和设计模式还很陌生,但我之前实现过一次单例模式,用于在不同的ViewController之间传递静态数组和字符串对象。我想知道是否有一种简单的方法可以让我的所有ViewController监听手势或事件,然后在所述... 查看详情