五、设计模式之单件(例)模式详解

设计模式目录:

五、设计模式之单件(例)模式详解

附录A 03:设计模式之责任链模式

一、什么是单件模式(Singleton Pattern)?

用来创建独一无二的,只能有一个实例的对象的入场券。
确保一个类只有一个实例,并提供一个全局访问点。

单件模式的类图可以说是所有设计模式的类图中最简单的,其类图上只有一个类。尽管从类设计的视角来说它很简单,但是实现上还是会遇到相当多的波折。

单件模式的类图:

五、设计模式之单件(例)模式详解_第1张图片

下面来详细探究一下。

1.1、有什么用处?

有一些对象其实我们只需要一个,比如:线程池(threadpool)、缓存(cache)、处理偏好设置和注册表(registry)的对象、日志对象等。事实上,这类对象只能有一个实例,如果有多个实例,就会导致许多问题产生。例如,程序异常、资源使用过量,或是不一致的结果。

1.2、有一些类的确应该只有一个实例,为什么不通过程序员之间互相约定或者用全局变量来实现?如,Java的静态变量就可以做到。

许多时候,程序员互相约定是可以做到,但是有更好的办法,应该使用更好的这种。单件模式是经过时间考验的方法,可以确保只有一个实例会被创建。单件模式也给了我们一个全局的访问点,和全局变量一样方便,又没有全局变量的缺点。

1.3、使用全局变量有什么缺点?

例如:如果将对象赋值给一个全局变量,那么你必须在程序一开始就创建好对象(这里也和JVM的实现有关,有些JVM的实现是:在用到的时候才创建对象),假如这个对象非常耗费资源,而程序在这次的执行过程中又一直没有用到,不就浪费了?利用单间模式,有时我们可以控制它去实现:需要时才创建对象。

单件模式听起来简单,但如何保证一个对象只能被实例化一次,并能够解决相应存在的问题,并不是那么简单可以说出来。

 

二、如何创建一个对象?

2.1、众所周知的:new MyObject();

2.2、经典的单件模式实现(注意:这种代码是有问题的,后文会详细讲到):

public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton() {

    }

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

}

我们用下图来分析阐释:

五、设计模式之单件(例)模式详解_第2张图片

2.3、到此,我们初步的来小结一下:

  • 单件模式的对象实例是独一无二的,在任何时刻都只有一个对象,可以确保程序中使用的全局资源只有一份
  • 经常被用来管理共享的资源,如数据库连接或者线程池
  • 能够确保这个对象是单件的原因是:没有公开的构造器,使用私有构造器创建对象。
  • 想要取得单件对象,必须“请求”得到一个实例,而不是自行实例化得到一个实例。类中有一个静态方法getInstance(),调用这个方法,就可以获取。这个对象可能是本次调用才创建出来的,也有可能是以前早就被创建出来了。

 

三、单件模式中存在的问题

3.1、多线程问题:当有两个线程同时执行上述代码时,此时可能会出现什么问题?

可以将上述代码用两个线程同时访问,会很容易看到,会被创建两个Singleton对象出来。

处理办法:

3.1.1、只要把getInstance()方法编程同步synchronized方法,多线程问题就可以轻易解决了

public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton() {

    }

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

}

这种方法的确可以轻易解决问题,但是同步会降低性能,从而又是一个性能问题。并且,这个单件对象的获取,只有在第一次获取时才会进行同步来避免多线程问题,如果按照上述同步方法,在接下来后的每次调用时,同步方法就是累赘。

小结:如果getInstance()的性能对于应用程序不是很关键,就直接用这种方法解决问题好了。你的程序能够接受这种性能上的损失,就用这种方式,但如果频繁被调用和使用,那么就要慎重考虑了。

3.1.2、使用“急切”创建实例,而不是用”延迟实例化“的做法

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

public class Singleton {

    private static Singleton uniqueInstance = new Singleton();

    private Singleton() {

    }

    public static Singleton getInstance() {
    
        return uniqueInstance;
    }

}

利用这个做法,就可以在JVM加载这个类时,立刻创建唯一的单件实例,JVM保证在任何线程访问uniqueInstance静态变量之前,一定先创建此实例。

 

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

利用双重检查加锁(double-checked locking),首先检查是否实例已经创建好了,如果尚未创建,“才”进行同步,这样一来,只有第一次会同步,这样就完美切合了要求。如下代码:(此种方式不适合jdk1.4及更早版本)

public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton() {

    }

    public static Singleton getInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();   
                }
            }
        }
        return uniqueInstance;
    }

}

分析:

五、设计模式之单件(例)模式详解_第3张图片

如果性能是你关心的重点,那么这个做法可以大大地减少getInstanc()的时间耗费。

 

四、总结(包含一些其他可能的问题)

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

红色第6条的解决办法之一:自行指定类加载器,并指定同一个类加载器

注:学习自《Head First 设计模式》

你可能感兴趣的:(设计模式)