Skip to content

单例模式

引言

单例模式(Singleton Pattern)是Java中最常见的设计模式之一,它确保在程序中某个类只有一个实例存在,并提供一个全局访问点来访问这个唯一实例

这种设计模式在需要频繁实例化但又希望限制实例数量的场景下非常有用,如数据库连接池、线程池、配置文件的读取等

单例模式分为两种:

  1. 饿汉式
  2. 懒汉式

实现方式

饿汉式

在类加载时就完成实例的初始化,这种方式简单且线程安全,但可能会浪费一些系统资源

1、静态变量方式

java
public class Singleton {
    // 构造私有化
    private Singleton() {}
    
    // 静态变量
    private static Singleton instance = new Singleton();
    
    // 外部接口
    public static Singleton getInstance() {
        return instance;
    }
}

2、静态代码块的方式

java
public class Singleton {
    // 构造私有化
    private Singleton() {}
    
    // 定义变量
    private static Singleton instance;
    // 赋值
    static {
        instance = new Singleton();
    }
    
    // 外部接口
    public static Singleton getInstance() {
        return instance;
    }
}

懒汉式

懒汉式则是在第一次需要时才进行实例的初始化,这种方式更加灵活,但需要注意线程安全的问题

1、普通方式

简单粗暴,但是在多线程环境下存在问题,如果多个线程同时进入if语句,还没执行 instance = new Singleton(); 前,都判断为true,那么会导致多个实例被创建

java
public class Singleton {
    // 构造私有化
    private Singleton() {}

    // 定义变量
    private static Singleton instance;
    
    // 加synchronized同步锁
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

2、普通锁方式

在并发的情况下,会有创建两个不一样的实例问题,加上synchronized同步锁解决问题,但是效率大大降低,只有第一次创建时写操作,其他都是读操作。

java
public class Singleton {
    // 构造私有化
    private Singleton() {}

    // 定义变量
    private static Singleton instance;
    
    // 加synchronized同步锁
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

3、双重检查锁方式

双重校验锁则是一种结合了前两者的优点的方式,它既能保证线程安全,又能避免不必要的资源浪费。

java
public class Singleton {
    // 构造私有化
    private Singleton() {}

    // 定义变量, jvm指令重排问题, 加上volatile关键字保证可见性和有序性
    private static volatile Singleton instance;

    public static Singleton getInstance() {
        // 第一次判断
        if (null == instance) {
            // 加锁, 对当前类字节码
            synchronized (Singleton.class) {
                // 再次判断
                if (null == instance) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

4、静态内部类的方式

类加载时不会创建内部类的空间

java
public class Singleton {
    // 构造私有化
    private Singleton() {}

    // 定义静态内部类
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    // 外部接口
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

优缺点

优点:

  1. 资源利用率高:由于单例模式只生成一个实例,减少了系统性能的开销,当一个对象需要频繁地创建、销毁,并且创建和销毁的性能开销较大时,单例模式可以显著提高性能
  2. 管理方便:单例模式只生成一个实例,有利于外界对它的访问,从而可以方便地对其进行管理。例如,一个项目需要一个共享的连接池时,单例模式可以很好地实现
  3. 避免频繁创建和销毁:对于一些需要频繁实例化然后销毁的对象,使用单例模式可以提高系统性能。因为这些对象只需创建一次,然后一直驻留在内存中,从而避免了频繁创建和销毁所带来的性能损耗

缺点:

  1. 扩展困难:单例模式由于没有抽象层,因此难以扩展。如果需要增加新的功能,可能需要修改单例类的源代码,这违反了“开闭原则”
  2. 不利于代码测试:在并行开发环境中,如果单例对象没有正确地完成线程同步机制,可能会发生多个单例对象,这将严重破坏单例模式的设计初衷。此外,单例模式在测试时可能会带来一些困难,因为单例类的构造函数是私有的,无法直接实例化,这可能需要一些特殊的手段来进行测试
  3. 隐藏类的依赖关系:单例模式可能会隐藏类的依赖关系,导致代码可读性降低。由于单例类负责创建和管理自己的实例,这使得其他类在调用单例类时,可能不清楚其依赖关系,从而增加了代码的复杂性
  4. 违反单一职责原则:单例模式将创建和管理实例的职责都放在同一个类中,这在一定程度上违反了单一职责原则。虽然这可以通过将单例模式的职责分解到不同的类中来解决,但这可能会增加系统的复杂性

综上所述,单例模式在资源利用率、管理方便性和避免频繁创建和销毁等方面具有优点,但在扩展困难、不利于代码测试、隐藏类的依赖关系和违反单一职责原则等方面也存在缺点。 因此,在使用单例模式时,需要根据具体的应用场景和需求进行权衡。

Java使用单例模式的类

1、RunTime

使用饿汉式的静态成员变量的方式

java
public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {
    }
    
    // ......
}

2、System

使用饿汉式的静态代码块的方式

java
public final class System {

    /* register the natives via the static initializer.
     *
     * VM will invoke the initializeSystemClass method to complete
     * the initialization for this class separated from clinit.
     * Note that to use properties set by the VM, see the constraints
     * described in the initializeSystemClass method.
     */
    private static native void registerNatives();

    static {
        registerNatives();
    }

    /** Don't let anyone instantiate this class */
    private System() {
    }
    
    // ......
}