设计模式之单例模式

389 阅读6分钟

什么是单例模式?

单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。

但从实现的角度来说涉及到非常多的方面。单例模式的实现在开发者中一直是个很有争议的话题。这里我们将学习单例模式原则,不同的单例实现方式和最佳实践。

单例模式的特点和应用

单例模式限制类的实例和确保java类在java虚拟机中只有一个实例的存在。

单例类必须提供一个全局的访问来获取类的实例。

单例模式用来日志,驱动对象,缓存和线程池。

单例设计模式也用在其他设计模式,例如抽象工厂,建造者,原型,门面等设计模式。

单例模式还用在核心java中,例如java.lang.Runtime, java.awt.Desktop

java单例模式

为了实现Singleton模式,我们有不同的方法,但它们都有以下共同的概念。

  • 私有构造方法限制从其他类初始化类的实例。

  • 私有静态变量与该类的实例相同。

  • 公有静态方法返回类的实例,这是提供给外部访问的全局访问点来获取单例类的实例。在以下的章节,我们将学习单例模式的不同实现方法。

实现的类型

  • 饿汉模式

  • 懒汉模式

  • 线程安全的单例

  • 静态内部类

  • 枚举单例

饿汉模式

饿汉模式就是当类加载时就创建该类的实例,这是创建单例类最容易的方法,避免了线程同步问题但是有个一弊端是创建了该实例但是客户端程序可能不使用这个实例,则会造成内存的浪费。

以下是静态初始化单例类的实现。

public class EagerInitializedSingleton {

    private static EagerInitializedSingleton instance  = new EagerInitializedSingleton();

    private EagerInitializedSingleton(){

    }

    public static EagerInitializedSingleton getInstance(){
        return instance;
    }
}

在大多数的情景,单例类的创建是为了如文件系统,数据库连接等资源的管理。我们应该避免过早的创建类的实例化,除非直到客户端调用getInstance()方法。
这种方法也没有提供异常处理的任何选项。

静态饿汉模式

静块初始化实现类似于饿汉模式初始化,但类的实例在静态代码块中创建并对异常进行处理。

public class StaticBlockSingleton {
    private static StaticBlockSingleton instance ;

    static {
        try{
            instance = new StaticBlockSingleton();
        }catch (Exception e){
            throw new RuntimeException("静态代码块中实例化失败");
        }

    }

    public static StaticBlockSingleton getInstance(){
        return instance;
    }

}

饿汉模式和静态初始化两种实现都是在实例在被使用之前就已经创建了,这不是最佳实践。
以下章节中,我们将会学习如何创建支持懒加载的单例类。

懒汉模式

相比之前的两种方法,懒加载就是当需要的时候再来创建该类的实例,而不是一开始就把实例创建好了。

public class LazyInitializedSingleton {
    private static LazyInitializedSingleton instance = null;

    private LazyInitializedSingleton(){

    }

    public static LazyInitializedSingleton getInstance(){
        if(instance == null){
            instance = new LazyInitializedSingleton();
        }
        return  instance;
    }

}

懒加载的实现在单线程环境中可以正常使用,但是在多线程环境中会引发一些问题。这将会破坏单例模式和线程会获取不同的单例对象(不能保证线程安全)

线程安全的懒汉单例

public class ThreadSafeSingleton {
    private static ThreadSafeSingleton instance = null;

    private ThreadSafeSingleton(){

    }

    //同步关键字synchronized
    public static synchronized ThreadSafeSingleton getInstance(){
        if(instance == null){
            instance = new ThreadSafeSingleton();
        }
        return  instance;
    }

}

上述实现能正常运行,并提供了线程安全,但它降低了程序的性能,因为synchronized关键字修饰的是getInstance()整个方法。 但我们需要它仅用于谁可能创建单独的实例的第一个线程数(阅读:Java同步)。为了避免这种每一次的额外开销,使用双检查锁定原理。在这种方法中,synchronized块使用if条件里面加上一个额外的检查,以确保只创建一个单独的类的实例。

public static ThreadSafeSingleton getInstanceUsingDoubleLocking(){
    //synchronized同步块,控制更细的粒度
    if(instance == null){//此层的控制允许第一个线程进入访问,避免以上情况(同步方法)每次的等待开销。
        synchronized (ThreadSafeSingleton.class) {
            if(instance == null){//此处校验单例是否已经被创建,确实只有一个实例的存在。
                instance = new ThreadSafeSingleton();
            }
        }
    }
    return instance;
}

如代码中所示,我们进行了两次if (instance == null)检查,这样就可以保证线程安全了。这样,实例化代码只用执行一次,后面再次访问时,判断if (instance == null),直接return实例化对象。优点:线程安全;延迟加载;效率较高。 推荐使用

静态内部类单例模式

这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。

类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

优点:避免了线程不安全,延迟加载,效率高。

public class BillPughSingleton {

    private static class SingleTonHelpClass{
        private static  final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance(){
        return SingleTonHelpClass.INSTANCE;
    }

}

当单例类被加载,SingletonHelper内部类没有加载到内存中,只有当调用getInstance()方法时,该类被载入并创建Singleton类的实例。这种单例类因为它不要求使用同步的方法,容易理解和实现。推荐使用

枚举单例

这种方法简单,便借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

public enum  EnumSingleton {
    INSTANCE;

    public static void doSomething(){
        //do something
    }
}