什么是单例模式?
单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
实现单例模式的思路是:一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。
单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率)。
认识单例模式
有一些对象其实我们只需要一个,比如说:线程池、缓存、处理偏好设置的对象,充当打印机、显卡等设备的对象。事实上,这类对象只能有一个实例,如果制造出多个实例,就会导致许多问题发生,如程序行为异常,资源使用过量或返回不一致的结果。
利用静态类变量、静态方法和适当的修饰符,不是也可以达到这种效果嘛?确实如此,但其有个缺点,你必须在程序一开始就创建好对象,万一这个对象非常耗费资源,而程序又在这次的执行过程中一直没用到它,就浪费了资源。
当然这有时候也算不上缺点,有时我们需要马上用到这个对象,就需要开始就创建好对象了,虽然鱼和熊掌不可兼得,但我们的单例模式却可以分别实现“鱼”或“熊掌”。
单例模式的由来
问:
如何创建一个对象?
答:``new MyObject() ;
问:
一旦有一个类,就可以多次实例化它,即new MyObject()
吗?
答:
是的,如果是公开的类,就可以。
问:
如果不是呢?(默认修饰符)
答:
那只有同一个包内的类可以实例化它,但其仍可以被实例化多次。
问:
那这样呢?使用私有的构造器:
public MyClass{
private Myclass(){}
}
答:
这样定义确实没毛病,但仅有私有构造器的类不能被实例化。
问:
那有可以使用私有的构造器的对象吗?
答:
有,它自己可以调用自己的构造器。。。
问:
为什么?
答:
因为必须有MyClass
类的实例才能调用其的构造器,但因为没有其他类能够实例化MyClass
,所以我们得不到这样的实例。这是“鸡生蛋,蛋生鸡”的问题。可以在MyClass
类型的对象上使用构造器,但在这之前必须要有一个这个类型的实例,但在产生实例之前,又必须在MyClass
实例内才能调用私有的构造器。。。。。
问:
那么使用这样的代码如何:
public MyClass{
public static MyClass getInstance(){}
}
MyClass
有一个静态方法,可以通过类名的方式调用:MyClass.getInstance;
将这些何在一起,是否就可以初始化一个MyClass
?
public MyClass{
private Myclass(){}
public static MyClass getInstance(){
return new MyClass();
}
}
答:
当然可以,这确实是实例化对象的一种方式,这样MyClass
类就可以只创建一个实例了。
定义单例模式
单例模式:确保一个类只有一个实例,并提供一个全局访问点。
类图如下:
单例模式分为懒汉式和饿汉式,懒汉式指的是在只有在第一次使用
Singleton.getInstance
的时候,才开始实例化该类,即其可以延迟实例化,代码如下:
public class Singleton {
private static Singleton singleton;
private Singleton(){
}
public static Singleton getInstance(){
if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
而饿汉式指的是JVM的类加载器加载该类的时候就立马实例化该类,代码如下:
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return singleton;
}
}
多线程暴露的问题
前面说过,在多线程中,如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。不过我们有3种解决方案:
①方案一:为了解决这个问题,我们可以在getInstance()
方法前面加一个synchronized
关键字,将其同步锁起来,这样就不会有两个线程同时进入这个方法。但是同步会降低性能,这又是一个新的问题,而且更严重的是:只有第一次执行该方法时,才真正需要同步,一旦设置到singleton
变量,就不再需要同步这个方法了,之后的每次调用,同步都是一个累赘。
当然,如果对性能不是很依赖,可以使用同步锁的方式,这样即简单又高效。但如果该程序需要使用在频繁运行的地方,这种方式就不合适了。
②方案二:如果应用程序总是立即创建并使用单例,或者在创建和运行方面的负担不太重,就可以摒弃“懒汉式”的方式,使用用“饿汉式”的单例模式!这样在一开始的时候就实例化了该类,不会纠结该不该用同步锁的问题。
③方案三:用“双重检查加锁”,在getInstance()
中减少使用同步。利用双重检查加锁,首先检查是否实例以及创建了,如果尚未创建,才进行同步,只有第一次会同步,代码如下:
public class Singleton {
//使用volatile关键字,确保当singleton变量被初始化为Singleton实例时,多个线程正确的处理singleton变量
private volatile static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
//只有第一次才彻底执行下面if语句的代码
if (singleton == null) {//检查实例是否存在,如果不存在,进入同步区块
synchronized (Singleton.class) {
if (singleton == null) {//在检查一次,如果仍是null,才创建实例
singleton = new Singleton();
}
}
}
return singleton;
}
}
如果性能是关注的重点,这个做法可以大大地减少getInstance()
的时间耗费。
参考资料
《HeadFirst设计模式》