[Java基础] 设计模式之单例模式

写在前面

作者简介:鲸海鹿林

博客主页:鲸海鹿林的主页

名言警句:keep calm and carry on

        本系列参照HeadFirst系列设计模式这本书,换言之,是 HeadFirst设计模式这本书的读书笔记,让我们一起学习吧!

 单件模式(Singleton Pattern)

定义 

疑惑与解答 

单件模式与全局变量

什么缺点?

单件模式的创建

考虑多线程 

使用synchronized关键字,将getInstance变为同步的方法

改善多线程


 单件模式(Singleton Pattern)

用于创建独一无二的,只有一个实例的入场券。 

定义 

 单件模式,又称单例模式,是一种只用一个类,且类的构造函数访问权限为private(私有)的一种设计,该设计的关键便在于如何去实例化这个构造函数为私有的类。

疑惑与解答 

是的,你没有看错,这个设计模式的主要内容(思想)便是如何去实例化一个“对象”,并保证该对象的唯一性。

 到这儿是不是有一个疑问,这么设计有什么作用吗?为什么要做这种感觉上毫无意义的事?颇有一种画蛇添足之感。

但是,在我们很多不曾或者很少关注过的地方,便需要去使用这种模式来确保程序运行过程中的职责唯一性。例如:线程池(threadpool),缓存(cache)和处理注册表(registry)的对象,充当打印机,显卡等设备的驱动对象等等,试想一下,在我们配置语言环境的时候,如果打印机的驱动程序有两个或多个对象,会发生什么。在这些仅需要也必须保持唯一性的地方,如果创建或生成了多个实例,将会造成程序的行为异常,资源过度使用或者是不一致的结果等等问题。

单件模式与全局变量

 看到上面的解释,理解了有些地方在整个系统的运行过程中只能有一个实例,同时出现了另外的一个疑问——全局变量好像也能办到这件事。

确实,全局变量与单例模式一样,也只会创建一个实例而且可以使用static关键字保证在整个程序的运行过程中仅仅会被初始化时实例化对象一次,即仅生成一个对象,有必要再为此而使用一种新的模式吗?

本质上,单例模式就是为这个唯一对象提供了一个全局的访问点,并摒弃了使用全局变量的缺点。

什么缺点?

全局变量的特点之一便是其生命周期与类相同,换句话说,当类生成的时候,该全局变量便需要初始化并实例化对象,这对一些极为消耗资源的对象来说是致命的。

在Java的书写规范中也曾提及过一点——在有需要的时候再去创建变量/实例,而使用全局变量去实例化一个短时间内并不需要或者是整个程序运行过程中都不会去使用的对象,将会是对内存资源的极大浪费。以打印机为例,当用户选择好要打印的页数和打印方式(彩印/黑白,单/双面等),突然发现要打印的文档版本不是最新需要打印的版本,将界面关闭,整个界面的运行过程中并不需要打印机的驱动程序的实例对象(还未到使用打印机的步骤),若是使用全局变量,在整个界面打开过程中,该实例对象便会一直占用资源。

这里使用的全局变量指的是在变量定义时便初始化变量,当然,利用静态类变量,静态方法和适当的访问修饰符确实也可以做到,但是,如何保证对象的唯一呢?这便不是三言两语可以说清楚的了。

单件模式的创建

在讲述单件模式的创建方式之前,我们先来回顾几个问题:

如何去创建一个对象?

如果另一个对象也想实例化MyObject可以吗?

因此一旦有一个类,我们是否可以多次实例化它?

可以私有化类的构造器吗?

有可以使用私有构造器的类吗?

new MyObject();

可以。

只要是公开的类,便可以;如果不是公开的,只有同一个包中的类,可以实例化它,但仍可以多次进行实例化。

可以,定义合法,而且枚举中用的就是私有的构造器,但是对于普通的类而言,貌似没有什么意义。

有的,类本身可以调用其私有构造器,但是依旧毫无意义,其他的类无法使用构造器实例化类,那么该类内的构造器便无法调用,就无法去生成类的实例。

 如果在类中声明一个静态方法,在方法中创建类的实例呢?

public static MyObject getInstance(){
    return new MyObject();
}

在其他的类中便可以通过类名(MyObject.getInstance())访问该方法来获取类的实例了。 

可是,依旧没有保证唯一性,只要多次调用方法便可以生成多个实例,单件模式的目的并没有达到。

public class MyObject{
    private static MyObject uniqueInstance;
    private MyObject(){}
    public static MyObject getInstance(){
        if(uniqueInstance == null){
            uniqueInstance = new MyObject();
        }
        return uniqueInstance;
    }
    //TODO...
}

 这样是否保证了唯一实例的产生?

可以,而且单件可以延迟实例化——当我们不需要实例的时候,该类永远不会产生(不调用getInstance方法之前都不会产生实例)

上面的代码在单线程模式下是没有问题,但是在多线程模式下便会出现运行的异常。

考虑多线程 

使用synchronized关键字,将getInstance变为同步的方法

public class MyObject{
    private static MyObject uniqueInstance;
    private MyObject(){};
    public static synchronized MyObject getInstance(){
        if(uniqueInstance == null){
            uniqueInstance = new MyObject();
        }
        return uniqueInstance;
    }
    //TODO...
}

 优点:迫使每个线程在进入getInstance方法之前,等候其他线程离开该方法,确保了不会有两个线程同时进入该方法。

缺点:每次调用该方法,都需要同步一次,而实质上,只有第一次需要创建实例时才需要同步,会严重降低程序性能。

如果对该方法的性能不做要求(或者该方法在整个程序运行过程中不会被多次调用),该缺点可以忽略不计。

改善多线程

急切创建实例

在静态初始化时便创建单件。 

public class MyObject{
    private static MyObject uniqueInstance = new MyObject();
    private MyObject(){}
    public static MyObject getInstance(){
        return uniqueInstance;
    }
    //TODO...
}

双重检查加锁

首先判断是否已经创建实例了,若尚未创建,才进行同步。 

public class MyObject{
    private static volatile MyObject uniqueInstance;
    private MyObject(){};
    public static MyObject getInstance(){
        if(uniqueInstance == null){
            synchronized (MyObject.class){
                if(uniqueInstance == null){
                    uniqueInstance = new MyObject();
                }
            }
        }
        return uniqueInstance;
    }
    //TODO...
}

 检查实例,若不存在,则进入同步区块;进入区块后,再次检查是否存在实例,若没有则创建实例。

使用volatile关键字确保:

        当uniqueInstance被初始化成MyObject实例时,多个线程可以正确的处理该变量。


版权声明:本文为qq_50531508原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。