欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java设计模式之单例模式

程序员文章站 2022-05-04 17:11:06
...

单件模式(Singleton Pattern) :用来创建独一无二的,只能有一个实例的对象的入场券。

单件模式的作用:

有一些对象其实我们只需要一个,如线程池(threadpool),缓存(cache),对话框,处理偏好设置和注册表(registry)的对象,日志对象,充当打印机,显卡等设备驱动程序的对象。事实上,这些类对象只能有一个实例,如果制造出多个实例,就会导致许多问题产生。如程序的行为异常,资源使用过量,或者是不一致的结果。

或许我们可以利用全局变量来做到,但是如果将对象赋值给一个全局变量,那么必须在程序一开始就创建好对象(这其实和实现有关,有些jvm的实现是:在用到的时候才创建对象。),万一这个对象非常耗费资源,而程序在这次的执行过程中又一直没用到它,着就会造成资源的浪费。利用单件模式,我们可以在需要时才创建对象。

剖析经典的单件模式实现

public class Singleton {
    //利用一个静态变量来记录Singleton类的唯一实例
    private static Singleton uniqueInstance;
    //把构造器声明为私有的,只有自Singleton类内才可以调用构造器
    private Singleton() {}
    //用getInstance()方法实例化对象,并返回这个实例
    public static Singleton getInstance() {
        if (uniqueInstatance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
    //Singleton是一个正常的类,具有一些其他用途的实例变量和方法
    //其它的有用方法
}
//如果uniqueInstance是空的,表示还没有创建实例,而如果它不存在,我们就利用私有的构造器
//产生一个Singleton实例,并把它赋值到uniqueInstance静态变量中。请注意,如果我们不需要这
//个实例,它就永远不会产生,这就是延迟实例化。
if (uniqueInstance == null) {
    uniqueInstance = new MyClass();
} //如果uniqueInstance不是null,就表示之前已经创建过对象。我们就直接跳到return语句
return uniqueInstance;

我们以一个工业强度巧克力控制器为例,我们可以发现代码写的非常小心。

public class ChocolateBoiler {
    private boolean empty;
    private boolean boiled;
    
    //代码开始时,锅炉是空的
    public ChocolateBoiler() {
        empty = true;
        boiled = false;
    }
    //在炉内填入原料时,锅炉必须是空的,一旦填入原料,就把empty和boiled标志设置好
    public void fill() {
        if (isEmpty()) {
            empty = false;
            boiled = false;
            //在锅炉内填满巧克力和牛奶的混合物
        }
    }
    //锅炉排出时,必须是满的(不可以是空的)而且是煮过的,排除完毕后,把empty标志设回true
    public void drain() {
        if (!isEmpty() && isBoiled)) {
            //排出煮沸的巧克力和牛奶
            empty = true;
        }
    }
    
    //煮混合物时,锅炉必须是满的,并且是没有煮过的,一旦煮沸后,就把boiled标志设为true
    public void boil() {
        if (!isEmpty && !isBoiled) {
            //将炉内物煮沸
            boiled = true;
        }
    }
    
    public boolean isEmpty() {
        return empty;
    }
    
    public boolean isBoiled() {
        return boiled;
    }
 }

下面我们把这个类设计成单例模式

public class ChocolateBoiler {
    private boolean empty;
    private boolean boiled;
    private static ChocolateBoiler chocolateBoiler;
    
    private ChocolateBoiler () {
        empty = true;
        boiled = false;
    }
    
    public static ChocolateBoiler getInstance () {
        if (chocllateBoiler == null) {
            chocolateBoiler =  new chocolateBoiler();
        }
        retrurn chocolateBoiler;
    }
    
    public void fill() {
        if (isEmpty()) {
            empty = false;
            boiled = false;
        }
    }
    
    //省略
}

定义单件模式

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

我们正在把某个类设计成自己管理的一个单独实例,同时也避免其他类再自行产生实例。要想取得单件实例,通过单件类是唯一的途径。
我们也提供这个实例的全局访问点:当你需要实例时,向类查询,它会返回单个实例,前面的例子利用延迟实例化的方式创建单件,这种做法对资源敏感的对象特别重要。

我们来看一下单例模式的类图
Java设计模式之单例模式

虽然以上代码尽力防止不好的事情发生,但是ChocolateBoiler的fill方法仍然允许在加热的过程中继续加入原料。

现在假设有两个线程同时执行这段代码

public static ChocolateBoiler getInstance () {
    if (uniqueInstance == null) {
        uniqueInstance = new ChocolateBoiler ();
    }
    return uniqueInstance;
}

如果没有安全机制,那么如果在检测到uniqueInstance为空的情况下,两个线程同时进入if为真语句,那么就会产生两个不同的对象,object1,object2。

那么我们怎么来解决这个问题呢 ,我们只要把getInstance变成同步(synchronized)方法,多线程灾难就可以轻易地解决了:

public class Singleton {
    private static Singleton uniqueInstance;
    private Singleton() {}
    public static synchronized Singleton getInstance () {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
    // 其它有用的方法
}

这样的做法可以解决问题,但是同步会降低性能,这又会带来另一个问题。事实上比我们所想象的更严重的问题是,只有第一次执行此方法时才真正需要同步。换句话说一旦设置好uniqueInstance变量,就不需要同步这个方法了。之后每次调用这次方法,同步便会成为我们的负担。

改善多线程

1.如果getInstance()的性能对应用程序不是很关键,就什么都别做

如果你的应用程序可以接受getInstance()造成的额外负担,就忘了这件事吧。同步getInstance()的方法既简单又高效。但是必须知道,同步一个方法可能造成程序执行效率下降100倍。因此,如果将getInstance()的程序使用在频繁运行的地方,我们可能就得重新考虑了。

2.使用“急切”创建实例,而不用延迟实例化的做法

如果应用程序总是创建并使用单件实例,或者在创建和运行时方面的负担不太繁重,你可能想要急切(eagerly)创建此单件,如下所示

public class Singleton {
    //在静态初始化器(static initializr)中创建单件,这段代码保证了线程安全(thread safe)
    private static Singleton uniqueInstance = new Singleton();
    private Singleton () {}
    public static Singleton getInstance () {
        //已经有实例了,直接使用
        return uniqueInstance;
    }
}

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

3.用 “双重检查加锁”, 在getInstance()中减少使用同步

利用双重检查加锁 (double-checked locking) , 首先检查是否实例已经创建了,如果尚未创建, 才进行同步,这样一来只有第一次会同步,这正是我们想要的。

public class Singleton {
    private volatile static Singleton uniqueInstance;
    //检查实例,如果不存在就进入同步区块,只有第一次才彻底执行这里的代码,确保第一次才发生同步
    if (uniqueInstance == null) {
        synchronized (Sigleton.class) {
            //进入区块后,再检查一次,如果仍是null,才创建实例 避免创建两个实例
            if (uniqueInstance == null ) {
                uniqueInstance = new Singleton();
            }
        }
    }
    return uniqueInstance;
}
//volatile关键词确保,当uniqueInstance变量被初始化成Singleton实例时,多个线程正确地处理
//uniqueInstance变量。

这里请注意:

双重加锁不适用于1.4及更早版本地java中,许多JVM对于volatile关键字地实现会导致双重检查加锁地失效。如果不能使用java5及更新版本,而必须要使用旧版地java,就请不要用此技巧实现单件模式。

重新审视巧克力工厂

接下来看一下单件模式三种方案对巧克力工厂所遇到的问题的适用性

1.同步getInstance方法:

这是保证可行的最直接做法,对于巧克力锅炉似乎没有性能的考虑,所以可以使用这个方法。

2.急切实例化

我们一定要用到一个巧克力锅炉,所以静态地初始化实例并不是不可以。虽然对于采用标准模式的开发人员来说,此做法可能稍微陌生一点,但它还是可行的。

3.双重检查加锁

由于没有性能上的考虑,所以这个方法似乎杀鸡用了牛刀,采用这个方法还得确保使用的是java5以上的版本。

两个类加载器可能有机会各自创建自己的单件实例

每个类加载器都定义了一个命名空间,如果有两个以上的类加载器,不同的类加载器可能会加载同一个类,从整个程序来看,同一个类会被加载多次,如果这样的事情发生在单件上,就会产生多个单件并存的怪异现象。所以当我们的程序有多个类加载器又同时使用了单件模式,需要小心。

这里有一个解决办法:自行指定类加载器,并指定同一个类加载器。

总结

  1. 单件模式 ------ 确保一个类只有一个实例,并提供全局访问点。
  2. 单件模式确保程序中一个类,最多只有一个实例。
  3. 单件模式也提供访问这个实例的全局点。
  4. 在Java中实现单件模式需要私有的构造器,一个静态方法和一个静态变量。
  5. 确定在性能和资源上的限制,然后小心地选择适当地方案来实现单件,以解决多线程地问题(我们必须认定所有的程序都是多线程的)。
  6. 如果不是采用java5及以上,双重加锁会失效。
  7. 如果使用多个类加载器,可能导致单件失效而产生多个实例。
  8. 如果使用JVM 1.2或之前的版本,必须建立单件注册表,以免垃圾回收器将单件回收。