软件设计模式之单例设计模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。它是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局访问节点。
优点
1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如数据库连接)。
2、避免对资源的多重占用(比如写文件操作)。
缺点
由于单例模式同时解决了两个问题, 所以违反了单一职责原则
:
1、保证一个类只有一个实例。 为什么会有人想要控制一个类所拥有的实例数量? 最常见的原因是控制某些共享资源 (例如数据库或文件) 的访问权限。
2、为该实例提供一个全局访问节点。 还记得用过的那些存储重要对象的全局变量吗? 它们在使用上十分方便, 但同时也非常不安全, 因为任何代码都有可能覆盖掉那些变量的内容, 从而引发程序崩溃。
和全局变量一样, 单例模式也允许在程序的任何地方访问特定对象。 但是它可以保护该实例不被其他代码覆盖。
实例
一个班级只有一个班主任
实现
所有单例的实现都包含以下三个相同的步骤:
- 在类中添加一个私有静态成员变量用于保存单例实例。
- 声明一个公有静态构建方法用于获取单例实例。
- 将类的构造函数设为私有。类的静态方法仍能调用构造函数,但是其他对象不能调用。
如果你的代码能够访问单例类, 那它就能调用单例类的静态方法。 无论何时调用该方法, 它总是会返回相同的对象。
代码
1、基本实现(饿汉模式)
public class Singleton {
/**
* 私有静态变量缓存实例
*/
private static Singleton instance = new Singleton();
/**
* 提供公有静态方法获取实例
*
* @return 类实例
*/
public static Singleton getInstance() {
return instance;
}
/**
* 私有化构造方法
*/
private Singleton() {
}
}
以上代码就是一个单例模式的实现,该实现在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。为了解决这一问题,我们可以在单例类的实现中加入延迟加载,即在我们首次调用单例类的时候,再去初始化类的对象。
根据上面的案例及解决方式,我们对单例设计模式又细分为两种:
饿汉式:类加载就会导致该单实例对象被创建
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
2、懒汉模式(线程不安全)
下面我们来看看懒汉式的单例模式怎么实现,其实很简单,不要在类加载的时候就创建对象,在首次调用的时候创建对象即可。
public class Singleton {
/**
* 私有静态变量缓存实例
*/
private static Singleton instance;
/**
* 提供公有静态方法获取实例
*
* @return 类实例
*/
public static Singleton getInstance() {
// 首次调用instance为null就会创建实例
if (instance == null) {
instance = new Singleton();
}
return instance;
}
/**
* 私有化构造方法
*/
private Singleton() {
}
}
单例模式不愧是最最简单的设计模式之一,代码就这么多,但是上面的单例实现模式存在一点问题,那就是在多线程的情况下线程不安全,极有可能会创建多个对象。
3、懒汉模式(双重检查锁,线程安全)
为了解决线程不安全问题,我们会使用一种双重检查锁
的方式来编写单例模式,代码如下:
public class Singleton {
/**
* 私有静态变量缓存实例
* volatile 解决双重检查锁模式带来空指针异常的问题
*/
private static volatile Singleton instance;
/**
* 提供公有静态方法获取实例
*
* @return 类实例
*/
public static Singleton getInstance() {
// 如果instance为null才会创建实例
if (instance == null) {
synchronized (Singleton.class) {
// 抢到锁之后再次判断是否为空
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
/**
* 私有化构造方法
*/
private Singleton() {
}
}
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题。
说完了最常用的双重检查锁方式,我们再来了解一下比较有趣的两种单例模式的实现。
4、静态内部类实现
静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被 static
修饰,保证只被实例化一次,并且严格保证实例化顺序。
public class Singleton {
/**
* 私有化构造方法
*/
private Singleton() {}
/**
* 静态内部类创建实例
*/
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
/**
* 提供公有静态方法获取实例
*
* @return 类实例
*/
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机才会加载SingletonHolder并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。
静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
5、枚举方式
枚举类实现单例模式是Effective Java 作者 Josh Bloch推荐的单例实现模式,也是最佳的实现方式(但是并未被广泛使用,原因可能是因为枚举是JDK1.5才有的吧,推出的时间比较晚,并且用的人也比较少),因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
public enum Singleton {
INSTANCE;
}
枚举方式属于饿汉式方式。
总结
看完了这么多的单例设计模式,是不是感觉有点不知道使用哪种实现方式比较好?
我个人一般情况下直接使用饿汉式的方式实现单例模式,消耗不了多少资源和性能。如果明确需要懒加载的话,可以使用双重检查锁方式或者静态内部类方式,如果涉及到序列化和反序列化,直接采用枚举方式,或者,你也可以直接采用枚举方式,都是个人习惯问题,没有硬性规定。