学习笔记之设计模式 --- 单例模式

学习笔记之设计模式 — 单例模式


0.前言

单例模式是常用的一种设计模式,也是最经典简单的一种设计模式;一些软件系统设计时就是建立在单例的基础之上的,所以可以在某些框架源码中看到。

1.起因

我们在软件开发过程中同样会遇到这样的问题:多个对象需要从一个类的实例获取一些信息,但如果它们得到的信息内容不一致,则意味着有多个状态在同一瞬间,然而其中只有一个状态是真实的,那么其他对象处理的都是错误信息。[s][]
在现实中,同样的某些重要信息都要交给一个特定的单位处理的;例如:

  • 国家政策和方针都是由唯一的政府体系决定(公司的管理也是这样)
  • 开车只需要一个司机(虽然可以给他指明方向,但不能同时驾驶一辆车)
  • 一台电脑只能运行一个真实系统(不知道现在实现没有)
  • 一个系统只有一个任务管理器

类比程序开发过程中,大多数管理设备就是被设计成单例模式:

  • IO设备(避免被同时访问),
  • 配置管理器(管理配置文件,防止属性歧义)
  • 资源访问记录管理器等等
    保证某一特定对象的唯一性就是为了解决这样的问题。

2.要点

虽然明确了采取单例的目的,但我们还需了解如何设计才能具体去实现。我们已经知道单例最基本的要求就是要让某一个类只有一个具体实例对象了,但要实现这一点,可以从客户端对其进行实例化开始。因此需要用一种只允许生成对象类的唯一实例的机制,“阻止”所有想要生成对象的访问。使用工厂方法来限制实例化过程。这个方法应该是静态方法(类方法),因为让类的实例去生成另一个唯一实例毫无意义。[linktext][2]
概括起来就有以下三个要点:

1.某个类只能有一个实例对象.
2.这个类需要自己创建这个实例.
3.这个类必须自己提供这个实例给外部调用.

而从实现的角度来看,我们可以以私有化构造函数隔绝新建实例,以私有静态的形式保存实例对象,以类方法的形式提供对象。

3.特点

去除实现单例之前所需要知道的三大要点外,它还有自身设计的优缺点:
优点:

1.实例控制

单例模式会阻止其他对象实例化其自己的单例对象的副本,从而确保所有对象都访问唯一实例。

2.灵活性

因为类控制了实例化过程,所以类可以灵活更改实例化过程。

缺点:

1.开销

虽然数量很少,但如果每次对象请求引用时都要检查是否存在类的实例,将仍然需要一些开销。可以通过使用静态初始化解决此问题。

2.混淆

使用单例对象(尤其在类库中定义的对象)时,开发人员必须记住自己不能使用new关键字实例化对象。因为可能无法访问库源代码,因此应用程序开发人员可能会意外发现自己无法直接实例化此类。

3.对象生存期

不能解决删除单个对象的问题。在提供内存管理的语言中(例如基于.NET Framework的语言),只有单例类能够导致实例被取消分配,因为它包含对该实例的私有引用。在某些语言中(如 C++),其他类可以删除对象实例,但这样会导致单例类中出现悬浮引用。

4.实现方式

单例模式的实现方式有多种,这里总结了几种大概的实现方式以及各自的优缺点:

  1. 懒汉式
  2. 饿汉式
  3. 懒汉线程安全式
  4. 静态内部类式
  5. 枚举式
懒汉式

正如其名很懒,就像我这种把事留到最后才做的懒人一样,总是在需要用实例时才初始化;懒汉和饿汉两种实现方式都是我们常见的,这里不再过多赘述,直接上代码:

class Singleton1{
    //类的私有静态对象
    private static Singleton1 instance = null;
    //私有化,防止从外部实例化
    private Singleton1(){}
    //提供唯一实例 instance
    public static Singleton1 getInstance(){
        //只有第一次访问时才创建对象
        if ( null == instance ){
            instance = new Singleton1();
        }
        return instance;
    }
}

从实际代码可以看出这种方式基本是符合三大要点的,

优点:

  • 实现简单
  • 只有在第一次调用getInstance() 时才创建对象,不使用则不会占空间,所以可以节约部分空间

缺点:

  • 如果对象很大,则第一次访问时会很慢
  • 没考虑到对线程安全
饿汉式

这个饿汉的意思大概就是有什么就马上吃掉, 和懒汉不同,这是个做事有准备的人:

class Singleton2{
    //随着类的初始化创建
    private static Singleton2 instance = new Singleton2();
    private Singleton2(){}
    public Singleton2 getInstance(){
        return instance;
    }
}

当然也可以把创建实例放入静态块中,两者效果几乎相等的;

class Singleton2{
    private static Singleton2 instance = null;
    static{
        instance = new Singleton2();
    }
    //...otherFunction
}

他们的优缺点也正好互补:

优点:

  • 同样实现简单
  • 天生线程安全
  • 不需要考虑手动实现线程安全所带来的性能损失

缺点:

  • 需要一开始就加载实例,即使不会用到,会给程序带来些许负担
懒汉线程安全式

为了解决懒汉方式的不足,需要加上线程安全机制,首先想到的肯定是synchronized关键字吧,但是根据修饰属性也有些不同,具体看下面两个例子:
1.直接修饰getInstance()方法

class Singleton3{
    private static Singleton3 instance = null;
    private Singleton3(){}

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

只有getInstance()被同步了,但是我们都知道:从外部访问synchronized的一次只能有一个,必须运行完才会允许下一个进入!

2.只同步创建实例的代码块

class Singleton3{
    private static volatile Singleton3 instance = null ;

    private Singleton3(){}

    public static Singleton3(){
        if( null == instance ){
            synchronized(Singleton3.class)){
                if( null == instance ){
                    instance = new Singleton3();
                }
            }
        }
        return instance;
    }
}

这里的instance用volatile修饰是为了让变量在线程间可见,而synchronized前后各用一个if 语句是为了防止在当前进程刚进入同步代码块时,instance变量恰好被刚离开的进程初始化了(不知道我说清楚没有),所以性能上比上一个更优。
这个方式的优缺点是:

优点:

  • 线程安全
  • 性能良好

缺点:

  • 实现繁琐
  • 不易理解
静态内部类方式

前面解决了懒汉式的线程安全问题,可以试着解决饿汉式的性能问题,将实例交给静态持有类就是一种很好的解决方案,只有当访问实例事才会加载静态类,而不会造成一开始的资源浪费。

class Singleton4{
    private static final class InstanceHolder{
        private static final Singleton4 instance = new Singleton4();
    }

    private Singleton4(){}
    public Singleton4 getInstance(){
        return InstanceHandor.instance;
    }
}

这种方式解决了懒汉式的问题,也算利用饿汉式,同时也继承了它们两个的优点。

优点: 
    1.线程安全
    2.性能优异
    3.延迟加载
缺点:
    会产生一个内部类,加载时会多出一个class文件

这些方式都是围绕着唯一实例初始化加载的时机来展开的,所以可以分为两个大方向:懒汉(延迟加载)和饿汉(立即加载),然后就是为了解决各自问题(线程安全和性能)的优化方案,以及结合了两者优点的方案。虽然基本解决了线程方面的问题,然而实际情况可能远比想象中的复杂,在JAVA 语言中就存在克隆,序列和反序列化等其他方式进行实例化,甚至是通过反射实例化,这里再介绍下面这种方式

枚举式

话不多说,先上代码感受一下:

enum  Singleton7{
    /* 单一实例 */
    INSTANCE;

    /* 对象的一系列方法... */
    public void methods(){
        //System.out.println("singleton enum");
    }
}

第一次见的时候我也有些懵了,什么鬼?实际上,枚举可以很好的防止克隆和序列化的实现,当然常规手法也可以实现,但是这种方式更加巧妙,我们所要的三个基本要点都被实现在一个枚举类型里了,不需要通过其他附加手段优化改进

优点:

  • 线程安全
  • 克隆和序列化无效
  • 本身不允许外部新建枚举实例
  • 结构清晰,实例和方法在一个层面

缺点:

  • 不易理解和说明, 可讀性不高
  • 日常開發中很少用到枚舉,

總結

以上方式各有各的好處,但缺點也是不可避免的,具體的選擇可以根據實際開發情景選擇,取決你項目本身
,是否在高并发环境工作,是否需要控制对象消耗,性能考虑等等。

猜你喜欢

转载自blog.csdn.net/desiyonan/article/details/78267588
今日推荐