在GoF的23种设计模式中,单例模式是比较简单的一种。然而,有时候越是简单的东西越容易出现问题。下面就单例设计模式详细的探讨一下。
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也推荐的这种方式。
分享到:
相关推荐
本篇文章将深入探讨两种常见的设计模式:单例模式和简单工厂模式。 首先,单例模式是一种限制类实例化的模式,确保在整个应用程序中只有一个实例存在。其核心动机是控制共享资源的访问,例如日志记录器或者数据库...
在阅读《设计模式浅析》这篇论文时,我们可以期待作者对这些模式的深入解析,包括它们的适用场景、如何在Java中实现,以及如何通过实例来理解它们的效果。此外,论文可能还会涉及设计原则,如开闭原则(对扩展开放,...
需要注意的是,在多线程的编程语言中,单例模式需要处理线程安全问题,例如Java可以通过synchronized关键字或者将实例化提前到类变量定义时来解决。然而由于PHP并不支持多线程,线程安全的问题在PHP中通常不需考虑。...
这里,`Singleton`类是不能被其他类继承的,确保了单例模式的安全实施。 此外,`final`关键字还与匿名内部类和局部变量紧密相关。在创建匿名内部类时,如果需要引用外部类的非静态变量,这个变量必须是`final`的。...
在实际开发中,我们还可以使用工厂方法、静态工厂方法、构造器链、单例模式等不同的方式来创建对象。每种方式都有其特定的适用场景和优缺点,开发者应根据需求选择合适的创建策略。 此外,对于性能敏感的应用,了解...
本资料"安卓Android源码——防止内存溢出浅析"将深入探讨如何在Android源码层面理解和预防内存溢出。 首先,了解Android内存结构是必要的。Android内存分为堆内存和栈内存,Java对象主要存储在堆中,而局部变量和...
以下是对Android防止内存溢出的深入浅析: 1. **Android内存管理机制** - **Dalvik/ART虚拟机**:Android系统使用Dalvik或ART虚拟机执行应用程序,它们都有自己的内存管理策略。 - **堆内存**:Java对象主要存储...
例如,将Context传递给静态成员或单例模式的类时,需要特别注意及时释放引用。 2. **资源的释放**:例如Bitmap对象,由于其占用内存较大,如果在使用后未正确释放,会消耗大量内存。在屏幕旋转等配置改变时,系统会...
3. **内存泄漏**:未被正确释放的资源可能导致内存泄漏,例如匿名内部类、静态引用、单例模式等常见场景。分析源码时,要特别关注可能导致长期持有对象的代码结构。 4. **内存溢出**:当应用程序请求的内存超过了...
例如,静态变量、单例模式、匿名内部类、非静态内部类持有外部类引用等都可能导致内存泄漏。因此,开发时应避免不正确的引用持有,尤其是在长生命周期的对象中。 2. **大对象与分配策略**:大对象(如位图、音频或...
在Android应用中,常见内存泄漏的场景包括静态变量、单例模式、匿名内部类、非静态内部类、线程和Handler等。通过分析源码,我们可以找出这些潜在的内存泄漏点并修复它们。 2. **Bitmap管理**:Bitmap是Android中...
首先,Spring与MyBatis的整合思路是通过Spring来管理SqlSessionFactory,确保其以单例模式运行。SqlSessionFactory是MyBatis的核心,用于创建SqlSession对象,它是执行SQL操作的入口。Spring通过生成代理对象,利用...