单例模式是应用最广的设计模式之一,就算是你没有听说过设计模式,但也一定听说过单例,这个模式也可能是很多初级程序员唯一会使用的涉及模式。在这个模式中,单例对象的类必须保证只有一个实例存在。即不能自由构造对象的情况,就是单例模式的使用场景。
设计模式的使用场景很常见,一般如果碰到如下情况时,就应该考虑使用单例:
确保某个类有且只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种类型的对象应该有且只有一个。例如,创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源,这时就应该考虑使用单例模式。
单例模式的实现不难,只要做到了如下几点,那么就是一个单例模式:
- 构造函数不对外开放,一般为private;
- 通过一个静态方法或者枚举返回单例类对象;
- 确保单例类的对象有且仅有一个,尤其是在多线程的环境下;
- 确保单例类对象在反序列化时不会重新构建对象;
实现一(单线程)
特点
- 构造方法让其private,在就堵死了外界利用new创建此类实例的可能;
- GetInstance()方法是获取本类实例的唯一全局访问点;
- 若实例不存在,则new一个新的实例,否则返回已有的实例;
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton GetInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
问题
多线程的程序中,多个线程同时访问Singleton类,调用GetInstance()方法,会有可能造成创建多个实例。
实现二(懒汉式)
特点
- 程序运行时创建一个静态只读的进程辅助对象;
- 在同一个时刻加了锁的那部分程序只有一个线程可以进入;
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
问题
每次调用GetInstance方法时都需要lock,会影响性能。
实现三(双重锁定)
特点
- 先判断实例是否存在,不存在再加锁处理
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;
}
}
实现四(饿汉式)
特点
- 在第一次引用类的任何成员时创建实例。公共语言运行库负责处理变量初始化。
public class Singleton {
private static Singleton ourInstance = new Singleton();
public static Singleton getInstance() {
return ourInstance;
}
private Singleton() {
}
}
缺点
饿汉式,即静态初始化的方式,在类一加载就实例化的对象,所以要提前占用系统资源。若未使用此单例,则会造成空间浪费。
实现五(静态内部类式)
特点
- 当第一次加载Singleton类时并不会初始化instance,只有在第一次调用Singleton的getInstance方法时才会导致instance被初始化。
- 第一次调用getInstance方法会导致虚拟机加载SingletonHolder类,这种方式不仅能够确保线程安全,也能够保证单例对象的唯一性,同时也延迟了单例的实例化,所以这时推荐使用的单例模式的实现方式。
public class Singleton {
private Singleton() {}
public static synchronized Singleton getInstance() {
return SingletonHolder.instance;
}
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
}
实现六(枚举法)
特点
- 枚举在Java中与普通的类是一样的,不仅能够有字段,还能够有自己的方法,最重要的是默认枚举实例的创建是线程安全的,并且在任何情况下它都是一个单例。
- 反序列化情况,当从磁盘序列化到内存时,即使构造函数是私有的,反序列化时依然可以通过特殊的途径去创建类的一个新的实例,相当于调用该类的构造函数。反序列化操作提供了一个很特别的钩子函数,类中具有一个私有的、被实例化的方法readResolve(),这个方法可以让开发人员控制对象的反序列化。也就是说,前面的几个单例并没有避免序列化时创建新的对象,而枚举则不存在这个问题
public enum Singleton {
INSTANCE;
private String name;
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
}
注意
序列化问题
传统单例写法,如果实现了序列化接口,他们就不能再保持单例,因为readObject() 方法每次会返回新的对象,所以你需要重写这个方法。
private Object readResolve(){
return INSTANCE;
}
修改数据混乱问题
可加volatile关键字,保证每次从内存中读写,防止多线程数据修改出现混乱。
private volatile static Singleton instance;