定义
define a class that has only one instance and provides a global point of access to it.
定义一个有且仅有一个实列的类,并对外提供一个全局访问点。
实列
在人类语言中,有很多类似于太阳、月亮、故宫这样的专有名词,它们所指称的现实对象有且只有一个,而不是像种类名词植物、动物一样可以指称一群相似的事物。
同样,因面向对象编程语言中的"类"指称的也是对象,所以它也会有这样的区分,编程中有且仅有一个实列的类被称之为单例,但它需要我们做一定的限制才能实现。
故事
前不久,我开发了一个计数器,用于统计网站的访问次数。
当用户访问网站时,控制器(Controller)的access操作会被触发,每次触发都会调用计数器(Counter)的increase操作,将访问次数加一。
伪代码如下:
/**计数器*/
public class Counter {
protected int count=0;
public void increase(){
count++;
}
}
/**控制器*/
public class Controller {
private final Counter counter;
public Controller(){
this.counter = new Counter();
}
public void access(){
counter.increase();
}
}
问题
故事中,因为counter是一个有状态的全局对象,如果我们允许Controller(客户端)使用new关键字创建任意数量的Counter实列,那么在多线程的环境中就很有可能导致统计不准确。
所以这种情况下,我们应该收回客户端new对象的权限,并且确保Counter类的实列有且仅有一个。
那有什么方式可以实现呢?这便是单例模式。
方案
单例模式是一种对象创建型设计模式,它将有且仅有一个对象的类称之为单例类。
在单例类中,它的构造函数是私有的,这样客户端就无法通过构造函数实列化它;
再者,它对外提供了一个静态的全局可访问的操作instance,来获取它的实列对象,该对象是在它自身中被实列化的。
这样,客户端只能通过全局访问操作获取该类的实列。
实现
单例模式的实现方式有懒汉式、饿汉式、双检锁、静态内部类、枚举等,下面我们依次来看看这几种实现方式。
饿汉式
public class Singleton{
//在Singleton类被加载时,就实列化对象instance
private static final Singleton instance = new Singleton();
//私有化构造函数,禁止外部类通过默认的构造函数实列化对象
private Singleton(){}
//把方法声明为static,对外提供一个访问对象的入口
public static Singleton getInstance(){
return instance;
}
}
饿汉式的策略,是不管对象有没有被使用到,只要类没加载了就实列化对象。
这可能会出现资源浪费的现象,如果资源创建之后,一直没有被使用,那么就会浪费内存空间。
但饿汉式是线程安全的,因为Jvm在加载类并初始化静态变量instance的时候,会保证整个过程是线程安全的。
懒汉式
public class Singleton {
//私有化构造函数,禁止外部类通过默认的构造函数实列化对象
private Singleton(){}
private static Singleton singleton = null;
//把方法声明为static,对外提供一个访问对象的入口
public static Singleton getSingleton(){
//第一次调用的时候,才创建对象
if(singleton == null){
//自行实列化对象
singleton = new Singleton();
}
return singleton;
}
}
懒汉式的创建策略是在对象第一次被使用时,才会去创建。这种"延迟加载"的策略,能有效的避免资源浪费的现象出现。
但是,它是非线程安全的,如果需要线程安全的懒汉式那么我们可以像下面一样给它加个同步锁。
public class Singleton {
//私有化构造函数,禁止外部类通过默认的构造函数实列化对象
private Singleton(){}
private static Singleton singleton = null;
//给方法加一个synchronized,保证其调用是线程安全的
public static synchronized Singleton getSingleton(){
//第一次调用的时候,才创建对象
if(singleton == null){
//自行实列化对象
singleton = new Singleton();
}
return singleton;
}
}
双检锁/双重校验锁(DCL,即 double-checked locking)
public class Singleton {
//volatile关键字,保证多线程之间的可见性,解决DCL失效问题
private volatile static Singleton singleton;
//私有化构造函数,禁止外部类通过默认的构造函数实列化对象
private Singleton (){}
public static Singleton getSingleton() {
//第一次check,避免对象创建后,进入同步代码块时的性能开销
if (singleton == null) {
synchronized (Singleton.class) {
//第二次check,避免其它等待进入代码块的线程,进入代码块时重复创建对象
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
双检锁采用的是同步代码块的方式来加锁,这样相对来说性能会上面的好,因为只有在客户端第一次调用的时候才加锁,而懒汉式是每次获取都加锁。
静态内部类
public class Singleton {
//私有化静态内部类
private static class SingletonHolder{
private static Singleton instance = new Singleton();
}
private Singleton(){}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
静态内部类的方式也有懒加载的效果同时也保证了线程安全,当外部类(Singleton)被初始化的时候,并不会同时初始化内部类(SingletonHolder)而是在getInstance被触发时,才会初始化内部类。
枚举
public enum Singleton {
//只定义一个枚举
INSTANCE;
public void operation() {}
}
枚举的实列方式比较简洁、优雅,并且是线程安全的而且还能支持序列化,但种方式在实际应用中还是比较少见。
结构
单例类角色(Singleton):它的构造函数是私有的,但向客户端提供了一个全局的访问入口getInstance,来获取它的唯一实列。
总结
程序中的类,默认是可以被多次实列化的,因此在我们要求一个类只允许存在一个实列时,应该考虑使用单例模式。
单例模式的实现方式有很多种,如果对性能要求高,那么我们可以使用双检锁的方式,如果期望简单、优雅的方式那么枚举和静态内部类是比较合适的方式,不论如何我们都应该根据具体的情况选择合适的方式。