单例模式


单例模式基本介绍

单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

单例模式的基本思路

实现单例模式的思路是:一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。

单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率)。

单例模式的实现

饿汉式

public class SingletonTest01{
    public static void main(String[] args) {
//        Singleton01 singleton01 = new SingletonTest01();
        Singleton01 singleton1 = Singleton01.getInstance();
        Singleton01 singleton2 = Singleton01.getInstance();
        System.out.println("singleton1:"+singleton1.hashCode());
        System.out.println("singleton2:"+singleton2.hashCode());
    }
}


class Singleton01 {
    // 静态变存储唯一的实例化对象
    private static final Singleton01 INSTANCE = new Singleton01();
    // 构造器私有化
    private Singleton01() {}
    // 提供唯一的实例化对象
    public static Singleton01 getInstance() {
        return INSTANCE;
    }
}

像这样子实现单例模式,似乎是已经足够了。但是这样子做的效率不是非常高,因为当我们创建这个类的时候,无论有没有使用到这个类的实例都会产生这个对象,所以说这样子的单例模式的代码不具有懒加载性,需要改进。不过值得鼓励的是,这样的代码在多线程的模式下,不会产生任何的混乱,这也算是他的一个优点吧。

输出结果:

singleton1:1534030866
singleton2:1534030866

懒汉式

public class SingletonTest02 {
    public static void main(String[] args) {
        Singleton02 singleton1 = Singleton02.getInstance();
        Singleton02 singleton2 = Singleton02.getInstance();
        System.out.println("singleton1:" + singleton1.hashCode());
        System.out.println("singleton2:" + singleton2.hashCode());
    }
}

class Singleton02{
    private static volatile Singleton02 INSTANCE;

    private Singleton02() {
    }

    public static Singleton02 getInstance() {
        if (INSTANCE == null) {
            // 给以下的代码快加上线程锁,只有一个线程可以进入到这个代码块
            synchronized (Singleton02.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton02();
                }
            }
        }
        return INSTANCE;
    }
}

代码说明:

  1. volatile关键词?

    这个关键词C++中也有使用,但是也是非常的少见,基本上没有人用。

    被volatile修饰的共享变量,就具有了以下两点特性:

    • 1.保证了不同线程对该变量操作的内存可见性;
    • 2.禁止指令重排序

    volatile这个东西基本上都和多线程有关系,这里就不作深究了。

  2. 两个if判断和synchronized(Singleton02.class)

    为了实现懒加载,我们要做的是在调用getInstance这个方法的时候才去构建这个对象,不过考虑到多线程的情况下。如果直接是这样子写的话

    public static Singleton02 getInstance(){
        if (INSTANCE == null){
            INSTANCE = new Singleton02();
        }
        return INSTANCE;
    }

    这个代码在单线程的情况下是一点问题都没有的。但如果是多线程的环境下,有多个线程同时进入了if语句就会构建许多的对象,这就不是单例模式了,所以说这样的代码不能够使用。

    那是不是加个锁就行了呢?

    public static synchronized Singleton02 getInstance(){
        if (INSTANCE == null){
            INSTANCE = new Singleton02();
        }
        return INSTANCE;
    }

    确实是这样子代码确实就能用了,但是又不出现了一个新的问题。线程的同步是要花很多时间的。你一个线程调用这个函数其他的线程都要等你这个线程,这显然是效率太低了

    那行,咋继续改,不给方法加锁,只是给new语句加锁不就完事了嘛。**

    public static Singleton02 getInstance(){
        if (INSTANCE == null){
            synchronized(Singleton02.class){
               INSTANCE = new Singleton02(); 
            }
        }
        return INSTANCE;
    }

    给整个if语句加锁就相当于是给整个方法加锁,所以说是没有用的。这里给new语句加锁。不能说是效率低的问题了,是直接错误的。事实上,完全有可以多个线程同时进入了if语句内,虽然new加锁了,那几个进入if语句的线程都会执行new语句,破坏了单例模式。

    那怎么改呢?这就到了我们代码中的那个双重检查锁了。

    public static Singleton02 getInstance() {
        if (INSTANCE == null) {
            // 给以下的代码快加上线程锁,只有一个线程可以进入到这个代码块
            synchronized (Singleton02.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton02();
                }
            }
        }
        return INSTANCE;
    }

    这时虽然也会有多个线程进入if语句,但是里面还有一个带锁的if,这时第一个线程进入创建对象之后,后面的线程因为if的判断就无法创建对象了,实现了单例模式。

    要非常注意的是,前面的INSTANCE要声明为volatile(JDK5以后的版本),不然这个模式使用的失败的。
    这里我去看了极客班的C++的设计模式的教程,如果没有volatile关键词的话,这个是错误的是不能够使用的。为什么呢?在INSTANCE = new Singleton02()这个语句中,正常我们会想这个应该会先分配内存,然后开始构造函数,再然后把地址赋给INSTANCE。但是由于编译器的优化以及各种其他的情况下,任何语言可能都不是这样子的。大多数情况下,可能是先分配内存,然后直接赋地址,最后再构造函数,也可以是先赋地址,然后再怎么怎么样的。这种情况被称为reorder。这是如果有一个线程进入第一个if,发现你已经赋给地址了,已经不是空了,所以直接就返回,但是构造函数还没有开始启动,这是会发生什么错误是不用说了。加上了volatile之后就是告诉编译器,别给我优化,别给我整这些reorder。
    C++11当中也引入了这个问题的解决方案,不过确实是有点儿复杂,这里就不多说什么了。

输出结果:

singleton1:1534030866
singleton2:1534030866

静态内部类

使用静态内部类的性质来实现线程的同步。静态内部类中的静态数据只有被类第一次被使用到的时候创建,而且这个还是线程安全的,这都是由JVM来保证的。所以说这种方法也是非常好滴。代码也很易懂,不用做什么说明了。

public class SingletonTest03 {
    public static void main(String[] args) {

        Singleton03 singleton1 = Singleton03.getInstance();
        Singleton03 singleton2 = Singleton03.getInstance();
        System.out.println("singleton1:" + singleton1.hashCode());
        System.out.println("singleton2:" + singleton2.hashCode());
    }
}

class Singleton03{
    private Singleton03() {}

    // 使用静态内部类
    private static class SingletonInstance {
        private static final Singleton03 INSTANCE = new Singleton03();
    }

    public static Singleton03 getInstance() {
        return SingletonInstance.INSTANCE;
    }
}

输出结果:

singleton1:664223387
singleton2:664223387

枚举式

使用了JDK1.5中添加的枚举来实现单例模式。不仅以及避免多线程同步的问题,而且还可以防止反序列化创建新的对象。而且这种方法也是《Effective Java》中推荐的方法。

public class SingletonTest04 {
    public static void main(String[] args) {
        Singleton04 singleton1 = Singleton04.INSTANCE;
        Singleton04 singleton2 = Singleton04.INSTANCE;

        System.out.println("singleton1:" + singleton1.hashCode());
        System.out.println("singleton2:" + singleton2.hashCode());
    }
}

enum Singleton04 {
    INSTANCE;
}

这个方法可谓是简单而又粗暴,而且枚举类也是类,里面也可以有各种方法,也可以有private数据等。这里就不展开对Java中enum的说明了。

输出结果:

singleton1:1534030866
singleton2:1534030866

C++中的单例模式

// ...

class lock
{
    public:
        lock();
        lock(lock const & l);
        ~lock();
        lock & operator =(lock const & l);
        void request();
        void release();
    // ...
};

lock::lock()
{
    // ...
}

// ...

lock::~lock()
{
    // ...
}

// ...

void lock::request()
{
    // ...
}

void lock::release()
{
    // ...
}

// ...

// assumes _DATA_TYPE_ has a default constructor
template<typename _DATA_TYPE_>
class singleton
{
    public:
        static _DATA_TYPE_ * request();
        static void release();

    private:
        singleton();
        singleton(singleton<_DATA_TYPE_> const & s);
        ~singleton();
        singleton<_DATA_TYPE_> & operator =(singleton<_DATA_TYPE_> const & s);
        static _DATA_TYPE_ * pointer;
        static lock mutex;
    // ...
};

template<typename _DATA_TYPE_>
_DATA_TYPE_ * singleton<_DATA_TYPE_>::pointer = nullptr;

template<typename _DATA_TYPE_>
lock singleton<_DATA_TYPE_>::mutex;

template<typename _DATA_TYPE_>
_DATA_TYPE_ * singleton<_DATA_TYPE_>::request()
{
    if(singleton<_DATA_TYPE_>::pointer == nullptr)
    {
        singleton<_DATA_TYPE_>::mutex.request();

        if(singleton<_DATA_TYPE_>::pointer == nullptr)
        {
            singleton<_DATA_TYPE_>::pointer = new _DATA_TYPE_;
        }

        singleton<_DATA_TYPE_>::mutex.release();
    }

    return singleton<_DATA_TYPE_>::pointer;
}

template<typename _DATA_TYPE_>
void singleton<_DATA_TYPE_>::release()
{
    if(singleton<_DATA_TYPE_>::pointer != nullptr)
    {
        singleton<_DATA_TYPE_>::mutex.request();

        if(singleton<_DATA_TYPE_>::pointer != nullptr)
        {
            delete singleton<_DATA_TYPE_>::pointer;

            singleton<_DATA_TYPE_>::pointer = nullptr;
        }

        singleton<_DATA_TYPE_>::mutex.release();
    }
}

template<typename _DATA_TYPE_>
singleton<_DATA_TYPE_>::singleton()
{
    // ...
}

// ...

int main()
{
    int * s;

    s = singleton<int>::request();

    // ...

    singleton<int>::release();

    return 0;
}

这个代码着实有点儿难懂,而且我现在还没有接触到C++中的多线程。所以说,代码先放在这儿吧。C++真的是一门高深莫测的语言!

Java库中的单例模式

Java.lang.Runtime

/**
 * Every Java application has a single instance of class
 * {@code Runtime} that allows the application to interface with
 * the environment in which the application is running. The current
 * runtime can be obtained from the {@code getRuntime} method.
 * <p>
 * An application cannot create its own instance of this class.
 *
 * @author  unascribed
 * @see     java.lang.Runtime#getRuntime()
 * @since   1.0
 */

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

    private static Version version;

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

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

    //.....
}

可以看到这个使用的第一个,典型的饿汉式的单例模式。因为这个类不需要多线程,所以也没有必要使用懒汉式,这个饿汉式就是perfect的啦~~

总结

  • 单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。
  • 当想实例化一个单例类的时候,必须要记住使用对应的获取对象的方法,而不是使用new。
  • 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或者耗费资源过多(即:重量级对象),但又经常用到的对象,工具类对象、频繁访问数据库或文件的对象(比如数据源session工厂等)。

一枚小菜鸡