设计模式05—单件模式

上一篇:《设计模式04—工厂模式》

用来创建独一无二的,只能有一个实例的对象的

应用场景

线程池,缓存,对话框,注册表的对象,日志对象,充当打印机,显卡等

为何使用单例模式

我们利用静态变量,静态方法和适当的访问修饰符也可以做到这一点,但是如果我们将对象赋给一个静态变量,那么我们必须在程序一开始就要创建好这个对象,如果这个对象十分浪费内存,而且程序在执行过程中又没有用到它,就会形成浪费,而单例模式就可以很轻松的实现在需要的时候创建。

剖析经典的单件模式实现


public class Singleton {
    //利用一个静态变量来记录Singleton类的唯一实例
    private static Singleton uniquerSingleton;

    private static Singleton getInstance() {//将构造器声明为私有,只有来自Singleton类内才可以调用构造器
        if (uniquerSingleton == null) {
            /*如果对象不存在,就利用私有构造器产生 一个实例,
            并把它赋值到uniquerSingleton静态变量中,
            如果我们不需要,它就永远不会产生,这就是延迟实例化*/
            uniquerSingleton = new Singleton();
        }
        return uniquerSingleton;
    }

}

由于Singleton没有公开的构造器(原因是将构造方法设置为私有)这就保证了要想获取Singleton对象,只能通过getInstance方法,所以Singleton对象永远只可能有一个

  • 说明: 先不创建实例,当第一次被调用时,再创建实例,所以被称为懒汉式。

  • 优点: 延迟了实例化,如果不需要使用该类,就不会被实例化,节约了系统资源。

  • 缺点: 线程不安全,多线程环境下,如果多个线程同时进入了 if (uniqueInstance == null) ,若此时还未实例化,也就是uniqueInstance == null,那么就会有多个线程执行 uniqueInstance = new Singleton(); ,就会实例化多个实例;
    与之相反的是饿汉式(线程安全)

public class Singleton {
    private static Singleton uniqueInstance = new Singleton();
    private Singleton() {
    }
    public static Singleton getUniqueInstance() {
        return uniqueInstance;
    }

}
  • 说明: 实现和 线程不安全的懒汉式 几乎一样,唯一不同的点是,在get方法上 加了一把 锁。如此一来,多个线程访问,每次只有拿到锁的的线程能够进入该方法,避免了多线程不安全问题的出现。

  • 优点: 延迟实例化,节约了资源,并且是线程安全的

  • 缺点: 虽然解决了线程安全问题,但是性能降低了。因为,即使实例已经实例化了,既后续不会再出现线程安全问题了,但是锁还在,每次还是只能拿到锁的线程进入该方***使线程阻塞,等待时间过长。
    后面会重新讲解

下面我们以巧克力工厂为例子来研究单例模式

巧克力工厂

现代化的巧克力工厂具备计算机控制的巧克力锅炉,锅炉做的事就是把巧克力和牛奶融在一起,然后送到下一个阶段以制造成巧克力棒。
这里有一个Choc-O-Holic公司的工业强度巧克力锅炉控制器。看看它的代码,你会发现代码写得相当小心,他们在努力防止不好的事情发生。
例如:排出500加仑的未煮沸的混合物,或者锅炉已经满了还继续放原料,或者锅炉内还没放原料就开始空烧。

/**
 * 巧克力锅炉
 */
public class ChocolateBoiler {
    private boolean empty;
    private boolean boiled;

    public ChocolateBoiler() {
        //初始时锅炉是空的,而且也不在沸腾状态
        this.empty = true;
        this.boiled = false;
    }

    public boolean isEmpty() {
        return empty;
    }

    public boolean isBoiled() {
        return boiled;
    }

    /**
     * 填充混合物
     */
    public void fill() {
        /*在锅炉内填入原料时,锅炉必须是空的,
        一旦填入原料就必须要把empty和boiled的状态设置好*/
        if (isEmpty()) {
            empty = false;
            boiled = false;
            //用于填充牛奶和巧克力
        }
    }
    /**
     * 加热混合物
     */
    public void boil() {
        if (!isEmpty() && !isBoiled()) {
            //将状态置为沸腾
            boiled = true;
        }
    }

    /**
     * 排出混合物
     */
    public void drain() {
        /*锅炉排出时必须是满的,而且是煮沸的,
        排出结束之后需要重新将empty状态设置为空*/
        if (!isEmpty() && isBoiled()) {
            empty = true;//排出煮沸的巧克力和牛奶
        }
    }
}

如果同时存在多个ChocolateBoiler实例,可能就会发生很糟糕的事情
下面我们将巧克力锅炉的实例改为单例来避免这些意外发生。
我们还是按照之前的思路,将构造函数设置为私有来隐藏掉,同时设置一个获取实例的函数来控制只产生一个ChocolateBoiler实例。


/**
 * 巧克力锅炉——单例模式
 */
public class ChocolateBoilerSingleton {
    private boolean empty;
    private boolean boiled;
    private static ChocolateBoilerSingleton uniqueInstance;

    private ChocolateBoilerSingleton() {
        //初始时锅炉是空的,而且也不在沸腾状态
        this.empty = true;
        this.boiled = false;
    }

    public ChocolateBoilerSingleton getInstance() {
        if (uniqueInstance == null) {
            System.out.println("创建一个独一无二的巧克力锅炉实例");
            uniqueInstance = new ChocolateBoilerSingleton();
        }
        System.out.println("返回已创建的巧克力锅炉实例");
        return uniqueInstance;
    }

    public boolean isEmpty() {
        return empty;
    }

    public boolean isBoiled() {
        return boiled;
    }

    /**
     * 填充混合物
     */
    public void fill() {
        /*在锅炉内填入原料时,锅炉必须是空的,
        一旦填入原料就必须要把empty和boiled的状态设置好*/
        if (isEmpty()) {
            empty = false;
            boiled = false;
            //用于填充牛奶和巧克力
        }
    }
    /**
     * 加热混合物
     */
    public void boil() {
        if (!isEmpty() && !isBoiled()) {
            //将状态置为沸腾
            boiled = true;
        }
    }
    /**
     * 排出混合物
     */
    public void drain() {
        /*锅炉排出时必须是满的,而且是煮沸的,
        排出结束之后需要重新将empty状态设置为空*/
        if (!isEmpty() && isBoiled()) {
            empty = true;//排出煮沸的巧克力和牛奶
        }
    }
}

接下来我们进行一下测试

public class Test {
    public static void main(String args[]) {
        ChocolateBoilerSingleton boiler = ChocolateBoilerSingleton.getInstance();
        boiler.fill();
        boiler.boil();
        boiler.drain();

        System.out.println("empty:" + boiler.isEmpty() + "\nboil:" + boiler.isBoiled());

        // will return the existing instance
        ChocolateBoilerSingleton boiler2 = ChocolateBoilerSingleton.getInstance();
    }
}

运行结果:
在这里插入图片描述

定义单件模式

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

类图:

在这里插入图片描述

单例模式的线程问题

ChocolateBoilerSingleton方法竟然允许在加热的过程中能够使用fill方法,这样就会导致原料的溢出和浪费。

处理多线程

我们只需要把getInstance()变成同步(Synchronized)方法,多线程的问题就可以迎刃而解

public class Singleton {
    //利用一个静态变量来记录Singleton类的唯一实例
    private static Singleton uniquerSingleton;

    /*通过新增synchronized关键字到getInstance方法中,我们迫使每个线程
     * 在进入这个方法之前,要先等候别的线程离开这个方法,也就是不允许 两个及两个以上的线程执行此方法*/
    private static synchronized Singleton getInstance() {//将构造器声明为私有,只有来自Singleton类内才可以调用构造器
        if (uniquerSingleton == null) {
            /*如果对象不存在,就利用私有构造器产生 一个实例,
            并把它赋值到uniquerSingleton静态变量中,
            如果我们不需要,它就永远不会产生,这就是延迟实例化*/
            uniquerSingleton = new Singleton();
        }
        return uniquerSingleton;
    }

}

但是这又会遇到另外一个问题就是性能问题,只有第一次执行此方法时,才真正需要同步,换句话说吗,一旦设置了uniquerSingleton变量,那么就不需要进行同步,之后的每一次同步都是累赘。

改善线程性能问题

为了要符合大多数Java应用程序,很明显地,我们需要确保单件模式能在多线程的状况下正常工作。但是似乎同步getInstance()的做法将拖垮性能,该怎么办呢?

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

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

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

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

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

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

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

public class SingletonVolatile {
    /*利用一个静态变量来记录Singleton类的唯一实例
     * volatile关键词确保当uniquerSingleton变量被初始化成SingletonVolatile时,
     * 多个线程正确的处理uniquerSingleton变量*/
    private static volatile SingletonVolatile uniquerSingleton;


    private static SingletonVolatile getInstance() {//将构造器声明为私有,只有来自Singleton类内才可以调用构造器
        if (uniquerSingleton == null) {
            /*如果对象不存在,就利用私有构造器产生 一个实例,
            并把它赋值到uniquerSingleton静态变量中,
            如果我们不需要,它就永远不会产生,这就是延迟实例化*/
            uniquerSingleton = new SingletonVolatile();
        }
        return uniquerSingleton;
    }

}

这种做法就可以大大减少getInstance()的时间耗费

  • 把单件类当成超类,设计子类,但是我遇到了问题:究竟可以不可以继承单件类?
    继承单件类会遇到的一个问题,就是构造器是私有的。你不能用私有构造器来扩展类。所以你必须把单件的构造器改成公开的或受保护的。但是这么一来就不算是“真正的”单件了、因为别的类也可以实例化它。如果你真把构造器的访问权限改了,还有另一个问题会出现。
    单件的实现是利用静态变量,直接继承会导致所有的派生类共享同一个实例变量,这可能不是你想要的。
    所以,想要让子类能工作顺利。基类必须实现注册表(Registry)功能。
    在这么做之前,你得想想,继承单件能带来什么好处。就和大多数的模式一样,单件不一定适合设计进入一个库中。

  • 为何优先使用单件模式而不是去定义全局变量
    在Java中,全局变量基本上就是对对象的静态引用。
    在这样的情况下使用全局变量会有一些缺点,我们已经提到了其中的一个:急切实例化VS延迟实例化。
    但是我们要记住这个模式的目的:确保类只有一个实例并提供全局访问。
    全局变量可以提供全局访问,但是不能确保只有一个实例。全局变量也会变相鼓励开发人员,用许多全局变量指向许多小对象来造成命名空间(namespace)的污染。单件不鼓励这样的现象,但单件仍然可能被滥用。

目前已总结的OO原则:

  • 封装变化
  • 多用组合,少用继承
  • 针对接口编程,不针对实现编程
  • 为交互对象之间的松耦合设计而努力
  • 类应该对外扩展开放,对修改关闭
  • 依赖抽象,不要依赖具体

要点:

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

下一篇:《设计模式06—命令模式》

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZNineSun

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值