设计模式--单例模式(十)

单例模式:确保一个类只有一个实例,并提供一个全局访问点;

  

知识点的梳理:

  • Java中实现单例模式需要私有的构造器,一个静态方法和一个静态变量;
  • 确定在性能和资源上的限制之后,在选择合适的方案来实现单例,以解决多线程的问题;
  • 如果使用多个类加载器,可能导致单例实现而产生多个实例;

      

  • 为什么需要独一无二的对象?
    • 有一些对象其实我们只需要一个,比如:线程池,缓存,对话框,处理偏好设置和注册表的对象,日志对象,充当打印机,显卡等设备的驱动程序的对象;
      • 这些对象只能有一个实例,如果制造出多个实例,就会导致许多问题产生,如:程序的行为异常,资源使用过量,或者是不一致的结果;
    • 为什么不利用全局变量(静态变量)来做呢?
      • 如果将对象赋值给一个全局变量,那么你必须在程序一开始就创建好对象(当然有些JVM的实现是:在用到的时候才创建对象)。如果这个对象非常耗费资源,而程序在这次的执行过程中又一直没用到它,就形成了浪费;
  • 单例模式的实现
    • 先来看看代码

public class Singleton {

//利用一个静态变量来记录Singleton类的唯一实例

private static Singleton uniqueInstance;

//把构造器声明为私有的,只有自Singleton类内才可以调用构造器

private Singleton(){}

//getInstance()方法实例化对象,并返回这个实例

public static Singleton getInstance(){

if(uniqueInstance == null){

uniqueInstance = new Singleton();

}

return uniqueInstance;

}

//Singleton当然是一个正常的类,它可以具有一些其他涌入的实例变量和方法

}

  • 看深入看下getInstance()方法的实现
  • 巧克力工厂
    • 现在有一台巧克力锅炉控制器,该控制器需要防止锅炉已经被填满还继续放原料,或者锅炉内还没放原料就开始空烧等情况;

public class ChocolateBoiler {

private boolean empty;

private boolean boiled;

public ChocolateBoiler(){

//代码开始的时候,锅炉是空的

empty = true;

boiled = false;

}

public void fill(){

//在锅炉内填入原料时,锅炉必须是空的。一旦填入原料,就把emptyBoiled标志设置好

if(isEmpty()){

empty = false;

boiled = false;

//在锅炉内填满巧克力和牛奶的混合物

}

}

public void drain(){

//锅炉排除时,必须是满的(不可以是空的)而且是煮过的。排除完毕后,把empty标志设回true

if(!isEmpty() && isBoiled()){

//排出煮沸的巧克力和牛奶

empty = true;

}

}

public void boil(){

//煮混合物时,锅炉必须是满的,并且是没有煮过的。一旦煮沸后,就把boiled标志设为true

if(!isEmpty() && !isBoiled()){

//将炉内物煮沸

boiled = true;

}

}

  

public boolean isBoiled() {

return empty;

}

  

public boolean isEmpty() {

return boiled;

}

}

  • 改进这个类,让它变为单例模式

public class ChocolateBoiler {

private boolean empty;

private boolean boiled;

public static ChocolateBoiler uniqueInstance;

private ChocolateBoiler(){

//代码开始的时候,锅炉是空的

empty = true;

boiled = false;

}

public static ChocolateBoiler getInstance(){

if(uniqueInstance == null){

uniqueInstance = new ChocolateBoiler();

}

return uniqueInstance;

}

public void fill(){

//在锅炉内填入原料时,锅炉必须是空的。一旦填入原料,就把emptyBoiled标志设置好

if(isEmpty()){

empty = false;

boiled = false;

//在锅炉内填满巧克力和牛奶的混合物

}

}

public void drain(){

//锅炉排除时,必须是满的(不可以是空的)而且是煮过的。排除完毕后,把empty标志设回true

if(!isEmpty() && isBoiled()){

//排出煮沸的巧克力和牛奶

empty = true;

}

}

public void boil(){

//煮混合物时,锅炉必须是满的,并且是没有煮过的。一旦煮沸后,就把boiled标志设为true

if(!isEmpty() && !isBoiled()){

//将炉内物煮沸

boiled = true;

}

}

public boolean isBoiled() {

return empty;

}

public boolean isEmpty() {

return boiled;

}

}

  • 定义单例模式
    • 我们正在把某个类设计成自己管理的一个单独实例,同时也避免其他类再自行产生实例。只能通过单例类来获取单例实例;
    • 我们提供了这个实例的全局访问点:当你需要实例时,向类查询,它会返回单个实例。前面的例子利用延迟实例化的方式创建单例。这种做法对资源敏感的对象特别重要;
    • 来看看类图:
  • 多线程的问题
    • 上面示例中的ChocolateBoiler的fill()方法在多线程环境下会出现问题;
    • 利用synchronized改善多线程同步问题

public class Singleton {

private static Singleton uniqueInstance;

//其它有用的实例化的变量

private Singleton(){}

//通过增加synchronized关键字到getInstance()方法中,我们迫使每个线程在进入这个方法之前,要先等候别的线程离开该方法。

//也就是说,不会有两个线程可以同时进入这个方法

public static synchronized Singleton getInstance(){

if(uniqueInstance == null){

uniqueInstance = new Singleton();

}

return uniqueInstance;

}

}

这样可以解决问题,但是同步会降低性能。而且,只有第一次执行此方法时,才真正需要同步。换句话说,一旦设置好uniqueInstance变量,就不再需要同步这个方法了。之后每次调用这个方法,同步都是一种累赘。那应该怎么办呢?

  • 如果getInstance()的性能对应用程序不是很关键,就什么都别做;
    • 如果应用程序可以接受getInstance()造成的额外负担,就忘了这件事吧;
  • 使用"急切"创建实例,而不用延迟实例化的做法;
    • 如果应用程序总是创建并使用单例,或者在创建和运行时方面的负担不太繁重,可以使用"饿汉"式创建单例:

public class Singleton {

//在静态初始化器中创建单例。这段代码保证了线程安全

private static Singleton uniqueInstance = new Singleton();

private Singleton(){}

public static Singleton getInstance(){

//已经有实例了,直接使用它

return uniqueInstance;

}

}

这种做法,可以依赖JVM在加载这个类时,马上创建此唯一的单例。JVM保证在任何线程访问uniqueInstance静态变量之前,一定先创建此实例

  • 用"双重检查加锁",在getInstance()中减少使用同步;
    • 利用"双重检查加锁",首先检查是否实例已经创建了,如果尚未创建,"才"进行同步。这样一来,只有第一次会同步,这就对了;

public class Singleton {

//volatile关键词确保,当uniqueInstance变量被初始化成Singleton实例时,多个线程正确地处理uniqueInstance

//volatile只能在Java1.5以上使用

private volatile static Singleton uniqueInstance;

private Singleton(){}

//检查实例,如果不存在,就进入同步区块

public static Singleton getInstance(){

//注意,只有第一次才彻底执行这里的代码

if(uniqueInstance == null){

synchronized (Singleton.class){

if(uniqueInstance == null){

//进入区块后,再检查一次。如果仍是null,才创建实例

uniqueInstance = new Singleton();

}

}

}

return uniqueInstance;

}

}

这个做法可以保证性能

  • 将问题带回巧克力工厂
    • 上面提了很多的方案,在此实例中,该选择哪个呢?
      • 同步getInstance()方法:
        • 这是保证可行的最直接做法。如果不对性能有要求可以选择;
      • 饿汉式:
        • 如果一定只需要一个实例对象,那么就使用这种方式;
      • 双重检查加锁:
        • 需要保证性能就使用这种方式;
  • 单例模式问题总结
    • 是否可以创建一个类,把所有的方法和变量都定义为静态的,把类直接当做一个单例?
      • 如果你的类自给自足,而且不依赖于复杂的初始化,那么可以这么做;
      • 但,因为静态初始化的控制权是在Java手上,这么做有可能导致混乱,特别是当有许多类牵涉其中的时候。这么做常常会发生一些BUG;除非你有绝对的必要使用类的单例,否则还是建议使用对象的单例,比较保险;
    • 那么类加载器(class loader)呢?两个类加载器可能有机会各自创建自己的单例。
      • 每个类加载器都定义了一个命名空间,如果有两个以上的类加载器,不同的类加载器可能会加载同一个类,从整个程序来看,同一个类会被加载多次。如果这样的事情发生在单例上,就会产生多个单例并存的怪异现象。所以,如果你的程序有多个类加载器又同时使用了单例模式,这个时候有一个解决办法:自行指定类加载器,并指定同一个类加载器;
    • 类应该只做一件事。如果类能做两件事,就会被认为是不好的OO设计。单例有没有违反这个样的观念呢?
      • 单例类不只负责管理自己的实例(并提供全局访问),还在应用程序中担当角色,所以也可以被视为是两个责任。
    • 单例类是否可当成超类,设计出子类?究竟可以不可以继承单例类?
      • 继承单例类会遇到的一个问题,就是构造器是私有的。你不能用私有的构造器来扩展类;
      • 所以要把单例的构造器改成公开的或受保护的。但这样就不算是"真正的"单例了。因为别的类也可以实例化它;
      • 如果真把构造器的访问权限改了,还有另一个问题。单例的实现是利用静态变量,直接继承会导致所有的派生类共享同一个实例变量。所以,想要让子类能工作顺利,基类必须实现注册表(Registry)功能;
    • 全局变量比单件模式差在哪里?
      • Java中,全局变量基本上就是对对象的静态引用。在这样的情况下使用全局变量会有一些缺点,上面已经提了其中的一个:急切实例化VS.延迟实例化。
      • 我们要记住这个模式的目的:确保类只有一个实例并提供全局访问。全局变量可以提供全局访问,但是不能确保只有一个实例。
      • 全局变量也会变相鼓励开发人员,用许多全局变量指向许多小对象来造成命名空间的污染。单例不鼓励这样的现象,但单例仍然可能被滥用。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值