单例模式是一种创建型设计模式,它规定了一个类只能有一个实例存在,并且这个类自身负责这个实例的创建和管理。这个模式确保了无论何时,所有对象都可以通过一个全局访问点来获取并使用同一个类的唯一实例,而无需显式地对该类进行实例化操作。单例模式来解决一个全局使用的类频繁地创建与销毁的问题,是通过判断系统是否已经有这个单例,如果有则返回,如果没有则创建来解决这个问题的。
单例模式的优点:在内存里只有一个实例,减少了内存的开销,避免对资源的多重占用。
单例模式的缺点:没有接口,不能继承,与单一职责原则冲突(一个类应该有且仅有一个职责,它的行为应当尽可能地保持高内聚,不包含过多相互关联的功能)。
基本步骤
1.创建一个Singleton类
public class SingleObject{
//创建SingleObject的一个对象
private static SingleObject instance = new SingleObject();
//让构造函数为private,这样该类就不会被实例化
private SingleObject(){}
//获取唯一可用的对象
public static SingleObject getInstance(){
return instance;
}
public void showMessage(){
System.out.pringln(“hello world”);
}
}
2.从singleton类获取唯一的对象
public class SingletonPatternDemo{
public static void main(String[] args){
//不合法的构造函数
//编译时错误:构造函数 SingleObject() 是不可见的
//SingleObject object = new SingleObject();
//获取唯一可用的对象
SingleObject object = SingleObject.getInstance();
//显示消息
object.showMessage();
}
}
3.执行程序,输出结果
hello world
单例模式的几种实现方式
1.懒汉式,线程不安全
这是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁synchronized,所以严格意义上它并不算单例模式。
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
2.懒汉式,线程安全
这种方式具备很好的延迟加载,能够在多线程中很好的工作,但是效率低,它的优点是第一次调用才初始化,避免内存浪费。缺点是必须使用synchronized加锁才能保证单例,但加锁会影响效率。
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
3.饿汉式
这种方式比较常用,但容易产生垃圾对象。在类定义时,就直接创建单例对象的实例,并且是静态的(static)。这意味着当类被加载到JVM时,单例实例就会立即被创建,哪怕这个实例实际上还没有被任何地方引用,为了防止外部通过构造函数直接创建实例,将构造函数设为私有(private),定义一个公共的静态方法(通常命名为getInstance()),用于返回那个已经创建好的单例实例。优点是没有加锁,执行效率会提高,缺点是类加载时就初始化,会浪费内存。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
4.双检锁/双重校验锁(DCL)
这种方式采用双锁机制,安全且在多线程情况下能保持高性能。实现原理是:先使用volatile关键字修饰变量,确保多线程环境下当一个线程修改了singleton的值,其他线程能够立即看到这个修改;在获取对象的方法内部进行两次判空检查,第一次检查是在进入同步代码块之前,这是一个简单的非阻塞检查,如果实例已经存在,直接返回实例,避免了不必要的同步开销。第二次检查则在同步代码块内部,在进入创建实例的代码前再次确认实例是否已被创建,这一步是因为在多线程环境下,即使第一次检查发现实例为null,但在获得锁之前,其他线程可能已经创建了实例,所以要再次检查。
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
补充:volatile确保多线程环境下的可见性和有序性。当一个线程修改了singleton的值,其他线程能够立即看到这个修改,同时禁止了JVM的指令重排,保证了对象实例化过程的原子性。
5.登记式/静态内部类
这种方式能达到双检锁方式一样的功效,但实现更简单。Singleton类中定义了一个静态内部类。这个静态内部类不会随着外部类的加载而加载,而是在其被首次引用时加载。静态内部类中声明了一个静态final变量 ,并直接实例化了外部类赋值给这个变量。这表示当静态内部类被加载时,外部类的实例将被创建。由于这个变量是final修饰的,所以一旦初始化完成,其引用将不再改变,这也保证了实例的唯一性。
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
6.枚举
这是实现单例模式的最佳方法。枚举类型的实例是在第一次加载枚举类时由JVM以线程安全的方式创建,所以天生就是线程安全的,这也保证保证枚举实例的创建过程是线程安全的,并且在内存中是唯一的,通过定义一个包含单个元素,的枚举类型Singleton,实际上就创建了一个全局唯一的Singleton实例。它的程序更简洁,不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,防止多次实例化
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
总结
一般情况下,不建议使用第1种和第2种懒汉方式,建议使用第3种饿汉方式。只有在要明确实现lazy loading效果时,才会使用第5种登记方式。如果涉及到反序列化创建对象时,可以尝试使用第6种枚举方式。如果有其他特殊的需求,可以考虑使用第4种双检锁方式。