设计模式-单例

  动机

  有时一个类只有一个实例是非常重要的。例如,在一个系统中应该只有一个窗口管理器(或者只有一个文件系统,又或者只有一个打印池)。通常单例用于集中管理内部或外部的资源,并提供一个全局访问点访问它们。

  单例模式是最简单的设计模式之一:它只调用一个类,这个类的职责就是自身实例化,确保它只创建一个实例;同时它给这个实例提供一个全局访问点。在这种情况下,可以在任何地方使用这个实例,而不用每次直接调用构造器。

 

  目的

  确保一个类只创建一个实例。

  为这个实例对象提供一个全局访问点。

 

  实现

  实现方法是在这个“单例”类中调用一个静态成员,一个私有构造器和一个静态共有方法,此方法返回这个静态成员的引用。

Singleton Implementation - UML Class Diagram

  单例模式定义一个 getInstance 操作,它暴露用户访问的唯一实例。getInstance() 的职责是在这个类的唯一实例未被创建时创建并返回这个实例。

class Singleton
{
    private static Singleton instance;
    private Singleton()
    {
        ...
    }

    public static synchronized Singleton getInstance()
    {
        if (instance == null)
            instance = new Singleton();

        return instance;
    }
    ...
    public void doSomething()
    {
        ...    
    }
}

  你可以注意到,上面的代码中 getInstance 方法会确保只有一个类的实例会被创建。构造器不应从类的外部访问来确保只能通过 getInstance 方法这一种方式来实例化类。

  getInstance 方法也用来为对象提供一个全部访问入口,它可以以下面的方式使用:

Singleton.getInstance().doSomething();

 

  适用范围和示例

  根据定义,单例模式应在必须只存在一个类型实例时使用,并且客户必须通过全局访问点访问这个实例。这有一些单例使用的真实场景:

  示例1  日志类

  单例模式用于设计日志类。此类通常作为一个单例实现,并在所有应用组件中提供一个全局日志访问点,而不必创建每次创建一个执行日志操作的对象。

  示例2  配置类

  单例模式用于设计为应用提供配置设定的类。通过实现配置类作为一个单例,不仅可以提供一个访问点,我们还能当作一个缓存对象来保持这个实例。当这个类被实例化(或当一个值被读取)时,这个单例将在它的内部结构中保持这个值。如果这个值是从数据库或者文件中读取的,这就可以避免每次在使用配置参数时重新加载这些值。

  实例3  在共享模式中访问资源

  它可以用在串口应用的设计中。假如说应用中有许多类,运行在多线程环境下,它需要串口操作。在这种情况下,带有同步方法的单件应该被用来管理所有串口上的操作。

  实例4  工厂作为一个单件实现

  我们假设我们设计一个有工厂的应用来产生新对象(账户,顾客,场所,位置对象),这些对象有他们的 id,在多线程环境下,如果工厂在两个不同的线程中实例化了两次,那两个不同的对象可能有重复的 id。如果我们实现一个单例的工厂就可以避免这个问题。常规做法是抽象工厂或者工厂方法和单例设计模式结合使用。

 

  特殊问题和实现

  用多线程时,线程安全的实现

  一个健壮的单例实现应该可以在任何条件下工作。这就是我们需要确保它能在多线程下工作的原因。如前面所示的示例中单例可以用于多线程应用来确保读/写同步。

 

  懒汉实例使用双重锁机制

//懒汉实例使用双重锁机制.
class Singleton
{
    private static Singleton instance;

    private Singleton()
    {
    System.out.println("Singleton(): Initializing Instance");
    }

    public static Singleton getInstance()
    {
        if (instance == null)
        {
            synchronized(Singleton.class)
            {
                if (instance == null)
                {
                    System.out.println("getInstance(): First time getInstance was invoked!");
                    instance = new Singleton();
                }
            }            
        }

        return instance;
    }

    public void doSomething()
    {
        System.out.println("doSomething(): Singleton does something!");
    }
}

 

  上面代码展现是一个线程安全的标准实现,但不是最安全的实现,因为当我们讨论性能时,同步的代价非常高。我们可以看到同步方法 getInstance 在对象创建后不需要再检查同步。如果我们看到单例对象已经被创建,我们只需要返回它而不需要使用任何同步区。它的优势在于在非同步区中检查对象是否为空,如果为空,在一个同步区中再次检查并创建这个对象, 这被称为双重锁机制。

   在这种情况下,当第一次调用 getInstance() 方法时,单例实例就被创建了。它被称为懒汉实例,它确保单例实例只在需要时才被创建。

  

  饿汉实例使用静态字段实现

//使用静态字段实现的饿汉实例.
class Singleton
{
    private static Singleton instance = new Singleton();

    private Singleton()
    {
        System.out.println("Singleton(): Initializing Instance");
    }

    public static Singleton getInstance()
    {    
        return instance;
    }

    public void doSomething()
    {
        System.out.println("doSomething(): Singleton does something!");
    }
}

 

  在下面实现中,单例对象在当类被加载而不是第一次使用时被实例化,由于实际上实例的成员被声明为静态成员。这就是为什么在这种情况下我们不必同步代码的任何部分。一旦这些保证对象是唯一的,这个类就被加载。

 

  受保护的构造器

  为了允许单例可以被子类继承可以使用一个受保护的构造器。这种技术有两个缺点导致单例继承不切实际:

  首先,如果构造器是受保护的,这意味着这个类可以通过同一个包中的其它类调用构造器来实例化。一个可能的避免的解决方案是为实例创建一个单独的包。

  其次,为了使用派生类,应该把现有所有 getInstance 调用由  Singleton.getInstance() 改为 NewSingleton.getInstance()。

  

  如果类通过不同的访问单例类加载器来加载多个单例实例

  如果一个类(同类,同包)通过两个不同类加载器加载,它们在内存中表现为两个不同的类。

 

  序列化

  如果单例类实现 java.io.Serializable 接口,当一个单例被序列化,然后不止一次的反序列化,这将会创建多个单例实例。为了避免这种情况,应实现 readResolve 方法。查看 java 文档中的 Serializable() 和 readResolve() 方法。

public class Singleton implements Serializable {
        ...

        // 此方法在一个对象反序列化后立即被调用.
        // 此方法返回单例实例.
        protected Object readResolve() {
            return getInstance();
        }
    }

 

  抽象工厂和工厂方法作为单例实现

  有一种特定情形,当一个工厂应该唯一时,当有两个工厂来产生对象可能会有不期望的效果时。为了确保工厂是唯一的,工厂应该作为单例来实现。这样做我们也可以避免在使用前就实例化类。

 

  热点

  多线程   当多线程应用中必须使用单例时应特别小心。

  序列化  当单例实现序列化接口,它们必须实现 readResolve 方法以避免有两个不同的对象。

  类加载器  如果通过两个不同的类加载器加载单例类,我们会有两个不同的类,分别对应不同的类加载器。

  通过类名表示全局访问点  单例实例是使用类名获得的。 乍一看,这是一种访问它的简便方式,但它不是很灵活。如果我们需要替换 Sigleton 类,代码中的所有引用都应该做相应的更改。

  

posted on 2016-01-29 14:59  Sky.Y.Chen  阅读(127)  评论(0)    收藏  举报