单例模式是GoF23种设计模式中创建型模式的一种,也是所有设计模式中最简单的一种。
单例模式是用来保证系统中某个资源或对象全局唯一的一种模式,比如系统的线程池、日志收集器等。它保证了资源在系统中只有一份,一方面节省资源,另一方面可以避免并发时的内存安全问题。
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return INSTANCE;
}
}
饿汉模式是在类加载的时候直接创建了对象,不存在线程安全问题,它的优点是简单方便,缺点是如果这个类在不需要创建单例对象的时候就会装载(比如这个类有其它静态方法被调用)的话,会造成浪费,不过只要遵循单一职责原则,一般不会有这种问题。
所以只要不存在奇怪情况,推荐用饿汉模式。
public class Singleton {
private static Singleton INSTANCE;
private Singleton() {
}
public static Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
与饿汉模式相比,变量定义上的final
被去掉,getInstance()
方法中增加了一次null
判断,很显然,有线程安全问题,与饿汉模式相比,懒汉模式可以在实例化过程中通过外部传递参数的形式控制实例化对象。
宁愿用饿汉模式也不要用懒汉模式,不推荐使用。
public class Singleton {
private volatile static Singleton INSTANCE;
private Singleton() {
}
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
与上面的懒汉模式相比,变量定义上增加了volatile
避免指令问题,getInstance()
方法中有加了锁,里面又加了一次null
判断,防止两个线程同时执行getInstance()
且两个线程第一个null
都判断通过,但一个线程还没拿锁就被另一个线程先拿了锁之后重复创建对象的情况。
与饿汉模式相比,几乎适用于所有情况,但复杂度上升。
如果需要兼顾大多数的情况,可以使用双重锁懒汉模式。
静态内部类模式
public class Singleton {
private Singleton() {
}
private static class SingletonInnerClass{
private static Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInnerClass.INSTANCE;
}
}
与饿汉模式相比,实例定义属性放到了内部类中,Singleton
类加载后并不会立即加载内部类,只有第一次调用getInstance()
方法后,内部类才会被加载,不存在线程安全问题,比饿汉模式适应了更多的情况,复杂度比双重锁懒汉模式低,和饿汉模式一样,外部无法控制实例初始化的过程,无法传递参数进去。
如果单例类需要在脱离单例对象的情况下使用,可以用静态内部类模式。
类只能同时被1个线程初始化,且在同一个加载器中,同一个类不会第二次初始化。所以饿汉模式和静态内部类模式都没有线程安全问题
public enum Singleton {
INSTANCE;
private String field1;
private Integer field2;
public void method1(){
// ...
}
public void method2(){
// ...
}
}
在java中,enum可以像一个正常的class一样定义属性和方法,可以实现接口,但不能继承。
虽然它不复杂,且无法通过反射搞破坏,但它无法适用于更广泛的情况。
如果单例模式可能被破坏,可以使用枚举单例模式。
上面5种方式中,比较推荐饿汉模式,它虽然可能有资源浪费,但这个可能性真的很小,最重要的是它实现起来非常简单。