说到单例模式大家应该都不陌生,就是程序在运行过程中,一个类只允许有一个实例存在于内存当中。例如线程池管理类、缓存管理类、某个模块内存管理类等。这些类之所以只允许一个实例,除了内存消耗方面的考虑外,还有程序正确性的考量。
例如,假设图片内存管理类有两个实例A和B,那在实例A和B上分别有两个内存缓存,假设把一张图片存放在A,去B拿的时候就拿到空了,显然不合理。
单例模式要求一个类永远只能返回同一个实例,实现的步骤有两个:
1.将类的构造方法的修饰符改为private,这样外部将无法实例化这个类;
2.该类对外提供一个获取实例的方法,内部有一个指向本类的对象引用,当引用为空时,实例化这个类并返回,否则直接返回引用。
下面给出一些单例模式的实现:
1.饿汉模式:在类加载的时候直接实例化【可使用】
public class Singleton {
private static volatile Singleton sInstance = new Singleton();
private Singleton() {
}
public static Singleton instance() {
return sInstance;
}
}
优点:实现简单,在类被加载时就实例化,避免了线程同步问题;
缺点:类加载时就实例化,没有懒加载,后面可能用不到这个实例,造成了内存资源的浪费。
2.懒汉模式(同步方法)【不推荐】
public class Singleton {
private static Singleton sInstance;
private Singleton() {}
public static synchronized Singleton instance() {
if (sInstance == null) {
sInstance = new Singleton();
}
return sInstance;
}
}
优点:懒加载,线程同步;
缺点:尽管JDK7对synchronized做了优化(偏向锁、轻量级锁、自旋锁、锁去除),但是即使对象已经实例化了,每次也都要进行同步操作,易造成堵塞,效率低。
3.懒汉模式(双重检查)【不推荐】
public class Singleton {
private static volatile Singleton sInstance;
private static Object mObject = new Object();
private Singleton() {
}
public static Singleton instance() {
if (sInstance == null) {
synchronized (mObject) {
if (sInstance == null) {
sInstance = new Singleton();
}
}
}
return sInstance;
}
}
优点:两次check null,中间加必要的同步,实现了线程安全,创建了对象。接下来当第一步判断不为空的时候,就直接返回了,效率也比较高。
缺点:注意到sInstance前面使用volatile关键字修饰了吗?
这里说下题外话,JVM在new一个对象的时候,有如下3个步骤:
- a. 给Singleton的实例分配内存
- b. 调用构造函数,初始化成员属性字段
- c. 将sInstance指向这块内存
首先这个过程不是原子操作,其次JVM允许指令重排,当执行的顺序是a - b - c的时候是没问题,但是当执行的顺序是a - c - b时,线程A执行到c,让出了CPU执行时间片,此时线程B判断sInstance是不为空,直接返回引用,但是此时对象还没创建,用的时候岂不是很尴尬?NPE很有可能即将随之而来。这就是DCL失效问题,这种问题难以跟踪、复现,出现的概率小。
volatile能在sInstance插入内存屏障,防止指令重排,使每次读取都得去主内存读取,因此能防止这样的问题。
但指令重排什么的是JVM为程序运行做的优化之一,这样子做不太好,并且每次都得去主内存读取,或多或少也影响到性能。这种优化在《Java 高并发程序设计》等书籍上被称为“丑陋的优化”,因此不推荐使用这种。
4. 静态内部类【推荐】
public class Singleton {
private Singleton() {
}
public static Singleton instance() {
return SHolder.sInstance;
}
private static class SHolder {
private static final Singleton sInstance = new Singleton();
}
}
这个看着跟饿汉模式有几分相似?是懒汉加载吗?
类的静态属性只会在第一次加载类的时候初始化,因此sInstance只会在第一次调用SHolder.sInstance,即外部调用instance()时,才会初始化。
优点:巧妙利用类的初始化时机,避免了线程同步问题(类在初始化时,其它线程无法进入),同时实现了懒加载;
5. 枚举【推荐】
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。可能是因为枚举在JDK1.5中才添加,所以在实际项目开发中,很少见人这么写过。