所谓单例模式,简单来说,就是在整个应用中保证只有一个类的实例存在。就像是Java Web中的application,也就是提供了一个全局变量,用处相当广泛,比如保存全局数据,实现全局性的操作等。
主妇淘宝
1. 最简单的实现
首先,能够想到的最简单的实现是,把类的构造函数写成private的,从而保证别的类不能实例化此类,然后在类中提供一个静态的实例并能够返回给使用者。这样,使用者就可以通过这个引用使用到这个类的实例了。
public class SingletonClass {
private static final SingletonClass instance = new SingletonClass();
public static SingletonClass getInstance() {
return instance;
}
private SingletonClass() {
}
}
如上例,外部使用者如果需要使用SingletonClass的实例,只能通过getInstance()方法,并且它的构造方法是private的,这样就保证了只能有一个对象存在。
2. 性能优化——lazy loaded
上面的代码虽然简单,但是有一个问题——无论这个类是否被使用,都会创建一个instance对象。如果这个创建过程很耗时,比如需要连接10000次数据库(夸张了…:-)),并且这个类还并不一定会被使用,那么这个创建过程就是无用的。怎么办呢?
为了解决这个问题,我们想到了新的解决方案:
主妇淘宝
public class SingletonClass {
private static SingletonClass instance = null;
public static SingletonClass getInstance() {
if(instance == null) {
instance = new SingletonClass();
}
return instance;
}
private SingletonClass() {
}
}
代码的变化有两处——首先,把instance初始化为null,直到第一次使用的时候通过判断是否为null来创建对象。因为创建过程不在声明处,所以那个final的修饰必须去掉。
我们来想象一下这个过程。要使用SingletonClass,调用getInstance()方法。第一次的时候发现instance是null,然后就新建一个对象,返回出去;第二次再使用的时候,因为这个instance是static的,所以已经不是null了,因此不会再创建对象,直接将其返回。
这个过程就成为lazy loaded,也就是迟加载——直到使用的时候才进行加载。
3. 同步
上面的代码很清楚,也很简单。然而就像那句名言:“80%的错误都是由20%代码优化引起的”。单线程下,这段代码没有什么问题,可是如果是多线程,麻烦就来了。我们来分析一下:
线程A希望使用SingletonClass,调用getInstance()方法。因为是第一次调用,A就发现instance是null的,于是它开始创建实例,就在这个时候,CPU发生时间片切换,线程B开始执行,它要使用SingletonClass,调用getInstance()方法,同样检测到instance是null——注意,这是在A检测完之后切换的,也就是说A并没有来得及创建对象——因此B开始创建。B创建完成后,切换到A继续执行,因为它已经检测完了,所以A不会再检测一遍,它会直接创建对象。这样,线程A和B各自拥有一个SingletonClass的对象——单例失败!
解决的方法也很简单,那就是加锁:
主妇淘宝
public class SingletonClass {
private static SingletonClass instance = null;
public synchronized static SingletonClass getInstance() {
if(instance == null) {
instance = new SingletonClass();
}
return instance;
}
private SingletonClass() {
}
}
是要getInstance()加上同步锁,一个线程必须等待另外一个线程创建完成后才能使用这个方法,这就保证了单例的唯一性。
4. 又是性能
上面的代码又是很清楚很简单的,然而,简单的东西往往不够理想。这段代码毫无疑问存在性能的问题——synchronized修饰的同步块可是要比一般的代码段慢上几倍的!如果存在很多次getInstance()的调用,那性能问题就不得不考虑了!
让我们来分析一下,究竟是整个方法都必须加锁,还是仅仅其中某一句加锁就足够了?我们为什么要加锁呢?分析一下出现lazy loaded的那种情形的原因。原因就是检测null的操作和创建对象的操作分离了。如果这两个操作能够原子地进行,那么单例就已经保证了。于是,我们开始修改代码:
public class SingletonClass {
private static SingletonClass instance = null;
public static SingletonClass getInstance() {
synchronized (SingletonClass.class) {
if(instance == null) {
instance = new SingletonClass();
}
}
return instance;
}
private SingletonClass() {
}
}
首先去掉getInstance()的同步操作,然后把同步锁加载if语句上。但是这样的修改起不到任何作用:因为每次调用getInstance()的时候必然要同步,性能问题还是存在。如果……如果我们事先判断一下是不是为null再去同步呢?
public class SingletonClass {
private static SingletonClass instance = null;
public static SingletonClass getInstance() {
if (instance == null) {
synchronized (SingletonClass.class) {
if (instance == null) {
instance = new SingletonClass();
}
}
}
return instance;
}
private SingletonClass() {
}
}
还有问题吗?首先判断instance是不是为null,如果为null,加锁初始化;如果不为null,直接返回instance。
这就是double-checked locking设计实现单例模式。到此为止,一切都很完美。我们用一种很聪明的方式实现了单例模式。
5. 从源头检查
下面我们开始说编译原理。所谓编译,就是把源代码“翻译”成目标代码——大多数是指机器代码——的过程。针对Java,它的目标代码不是本地机器代码,而是虚拟机代码。编译原理里面有一个很重要的内容是编译器优化。所谓编译器优化是指,在不改变原来语义的情况下,通过调整语句顺序,来让程序运行的更快。这个过程成为reorder。
要知道,JVM只是一个标准,并不是实现。JVM中并没有规定有关编译器优化的内容,也就是说,JVM实现可以自由的进行编译器优化。
下面来想一下,创建一个变量需要哪些步骤呢?一个是申请一块内存,调用构造方法进行初始化操作,另一个是分配一个指针指向这块内存。这两个操作谁在前谁在后呢?JVM规范并没有规定。那么就存在这么一种情况,JVM是先开辟出一块内存,然后把指针指向这块内存,最后调用构造方法进行初始化。
下面我们来考虑这么一种情况:线程A开始创建SingletonClass的实例,此时线程B调用了getInstance()方法,首先判断instance是否为null。按照我们上面所说的内存模型,A已经把instance指向了那块内存,只是还没有调用构造方法,因此B检测到instance不为null,于是直接把instance返回了——问题出现了,尽管instance不为null,但它并没有构造完成,就像一套房子已经给了你钥匙,但你并不能住进去,因为里面还没有收拾。此时,如果B在A将instance构造完成之前就是用了这个实例,程序就会出现错误了!
于是,我们想到了下面的代码:
public class SingletonClass {
private static SingletonClass instance = null;
public static SingletonClass getInstance() {
if (instance == null) {
SingletonClass sc;
synchronized (SingletonClass.class) {
sc = instance;
if (sc == null) {
synchronized (SingletonClass.class) {
if(sc == null) {
sc = new SingletonClass();
}
}
instance = sc;
}
}
}
return instance;
}
private SingletonClass() {
}
}
我们在第一个同步块里面创建一个临时变量,然后使用这个临时变量进行对象的创建,并且在最后把instance指针临时变量的内存空间。写出这种代码基于以下思想,即synchronized会起到一个代码屏蔽的作用,同步块里面的代码和外部的代码没有联系。因此,在外部的同步块里面对临时变量sc进行操作并不影响instance,所以外部类在instance=sc;之前检测instance的时候,结果instance依然是null。
不过,这种想法完全是错误的!同步块的释放保证在此之前——也就是同步块里面——的操作必须完成,但是并不保证同步块之后的操作不能因编译器优化而调换到同步块结束之前进行。因此,编译器完全可以把instance=sc;这句移到内部同步块里面执行。这样,程序又是错误的了!
6. 解决方案
说了这么多,难道单例没有办法在Java中实现吗?其实不然!
在JDK 5之后,Java使用了新的内存模型。volatile关键字有了明确的语义——在JDK1.5之前,volatile是个关键字,但是并没有明确的规定其用途——被volatile修饰的写变量不能和之前的读写代码调整,读变量不能和之后的读写代码调整!因此,只要我们简单的把instance加上volatile关键字就可以了。
public class SingletonClass {
private volatile static SingletonClass instance = null;
public static SingletonClass getInstance() {
if (instance == null) {
synchronized (SingletonClass.class) {
if(instance == null) {
instance = new SingletonClass();
}
}
}
return instance;
}
private SingletonClass() {
}
}
然而,这只是JDK1.5之后的Java的解决方案,那之前版本呢?其实,还有另外的一种解决方案,并不会受到Java版本的影响:
public class SingletonClass {
private static class SingletonClassInstance {
private static final SingletonClass instance = new SingletonClass();
}
public static SingletonClass getInstance() {
return SingletonClassInstance.instance;
}
private SingletonClass() {
}
}
在这一版本的单例模式实现代码中,我们使用了Java的静态内部类。这一技术是被JVM明确说明了的,因此不存在任何二义性。在这段代码中,因为SingletonClass没有static的属性,因此并不会被初始化。直到调用getInstance()的时候,会首先加载SingletonClassInstance类,这个类有一个static的SingletonClass实例,因此需要调用SingletonClass的构造方法,然后getInstance()将把这个内部类的instance返回给使用者。由于这个instance是static的,因此并不会构造多次。
由于SingletonClassInstance是私有静态内部类,所以不会被其他类知道,同样,static语义也要求不会有多个实例存在。并且,JSL规范定义,类的构造必须是原子性的,非并发的,因此不需要加同步块。同样,由于这个构造是并发的,所以getInstance()也并不需要加同步。
至此,我们完整的了解了单例模式在Java语言中的时候,提出了两种解决方案。个人偏向于第二种,并且Effiective Java也推荐的这种方式。
分享到:
相关推荐
源代码则让我们能够看到这些模式在实际项目中的应用,通过阅读和分析代码,我们可以更好地掌握如何在自己的项目中运用这些模式。文档报告则提供了理论背景和使用场景,帮助我们深入理解设计模式的原理和价值。 总之...
1. **单例模式**:在Qt中,QApplication和QQmlEngine等类就是单例模式的应用,确保在整个程序中只有一个实例存在。 2. **工厂模式**:QFactoryInterface和QPluginLoader提供了一种动态创建对象的方式,允许在运行时...
例如,单例模式用于控制文本编辑器的唯一实例,工厂模式用于创建不同类型的文本操作对象。源代码中的设计模式值得我们深入研究和学习。 通过对C#文本编辑大师的源代码进行学习,开发者不仅可以掌握C#语言的基本语法...
例如,工厂模式可以改善对象创建的方式,单例模式用于控制类的实例化,而观察者模式则能增强组件之间的通信。理解并应用这些模式,能够使遗留代码更符合面向对象的原则,降低维护难度。 总之,这篇博客和附带的资源...
1. **创建型模式**:这类模式主要关注对象的创建,如单例模式(Singleton)、工厂模式(Factory)、抽象工厂模式(Abstract Factory)、建造者模式(Builder)和原型模式(Prototype)。在企业应用中,它们可以帮助...
7. **设计模式**:设计模式是解决常见问题的最佳实践,如单例模式、工厂模式等。在项目实践中,学习者可以通过实现设计模式来提高代码质量。 8. **调试技巧**:学会使用IDE(如Visual Studio或Code::Blocks)中的...
这23种模式包括创建型、结构型和行为型三大类别,如单例模式、工厂方法、抽象工厂、建造者模式、原型模式、适配器模式、装饰器模式、代理模式、桥接模式、组合模式、享元模式、外观模式、职责链模式、命令模式、解释...
7. **设计模式**:代码库可能展示了多种设计模式的应用,如工厂模式、单例模式、观察者模式等,这些都是解决常见问题的标准方案。 8. **编码规范**:开源代码通常遵循一定的编码风格,这对新手来说是很好的学习资源...
7. **设计模式**:在大型项目中,代码可能会运用到常见的设计模式,如单例模式、工厂模式、装饰器模式等,这些模式提供了解决常见问题的标准化解决方案。 8. **前端开发**:如果包含HTML、CSS和JavaScript文件,这...
5. **设计模式**:为了保证代码的可维护性和扩展性,开发者可能会采用一些设计模式,如工厂模式(用于创建服务器或客户端实例)、单例模式(确保服务器端只有一个实例)或观察者模式(用于实现消息的发布和订阅)。...
该项目提供了完整的源代码,同时附带了相关的参考文献,对于学习者来说,是一个很好的实践案例和学习资源。 在项目中,JSP作为前端展示,用于用户交互,如输入查询条件、显示结果等。JSP页面结合HTML、CSS和...
6. **源代码分析**: 随书附带的源代码可以帮助读者更好地理解和应用所学知识。这些代码示例涵盖了各种实际问题的解决方案,读者可以通过阅读和运行代码来深化理解。 7. **工程实践与设计模式**: 除了技术细节,本书...
书中的重点之一可能是对Gang of Four(GoF)23种经典设计模式的讲解,如工厂模式、单例模式、建造者模式、原型模式、适配器模式、装饰器模式、桥接模式、组合模式、装饰模式、代理模式、命令模式、责任链模式、解释...
本书涵盖了多种常见的设计模式,如单例模式、工厂模式、观察者模式、适配器模式、代理模式、建造者模式、装饰者模式、策略模式、职责链模式等。 1. **单例模式**:在Android中,例如Application类的实例就是典型的...
9. **设计模式**:源代码可能应用了常见的设计模式,如工厂模式、单例模式、观察者模式等,这对于提升代码的可读性和可维护性至关重要。 10. **工程管理和版本控制**:Delphi支持版本控制系统如Git,源代码可能包含...
7. 设计模式与最佳实践:项目的源代码可能会展示一些设计模式,如工厂模式、单例模式等,以及良好的编程习惯和注释规范。 8. 论文部分:附带的论文可能会详细阐述项目的背景、需求分析、系统设计、实施过程以及性能...
10. **软件设计模式**:书中实例可能采用了常见的设计模式,如工厂模式、单例模式等,这些模式有助于代码的复用和模块化。 总之,《Visual C/C++系统开发典型实例解析》不仅讲解了C++编程语言的基础和高级特性,还...
7. **设计模式**:源代码中常见的设计模式有单例模式、工厂模式、观察者模式等。设计模式是解决常见编程问题的经验总结,理解并应用它们可以提高代码的可读性和可维护性。 8. **反射机制**:Java的反射机制允许我们...
在Python中,我们可以应用如工厂模式、单例模式、装饰器模式等经典设计模式。例如,工厂模式可以用来创建对象,而单例模式确保一个类只有一个实例。装饰器模式则允许我们动态地修改或扩展函数和类的行为,而无需改动...
8. **设计模式**:《C++编程思想》也涵盖了设计模式,如工厂模式、单例模式、观察者模式等,这些都是软件设计中常见的可复用解决方案。 源代码文件列表“C++编程思想源代码”可能包含了上述所有或部分知识点的实例...