前言
对系统中的某些类来说,只有一个实例很重要,例如我们常见的任务管理器,大家可以试试看能不能打开两个任务管理器,又比如Spring中的bean有singleton模式。单例模式是目的为了节约系统资源,有时需要确保系统中某个类只有唯一一个实例,当这个唯一实例创建成功之后,我们无法再创建一个同类型的其他对象,所有的操作都只能基于这个唯一实例。
单例模式的要点有三个:
- 是某个类只能有一个实例;
- 是它必须自行创建这个实例;
- 是它必须自行向整个系统提供这个实例。
单例模式的实现方式
单例模式有多种实现方式,我们一种一种来分析:
1. 通过静态公有工厂方法创建单例
想必大家应该很少看到私有的构造函数吧,私有构造函数就意味着在外部不能创建这个实例,也就不可能产生“多例”了!那要如何获取单例对象呢?我们可以在类中定义一个静态公有工厂方法,返回唯一实例,代码如下:
public class Singleton
{
private static Singleton instance=null; //静态私有成员变量
//私有构造函数
private Singleton(){}
//静态公有工厂方法,返回唯一实例
public static Singleton getInstance(){
if(instance==null)
instance=new Singleton();
return instance;
}
}
在每次返回实例时,我们都判断静态私有成员变量是否为空,如果不为空就代表着以及存在了该单例,直接将自身的静态变量返回,如果为空则说明还没有创建过该对象,先创建后再返回。
在这种实行方式下,我们有以下几点需要注意:
- 单例类的构造函数必须为私有;
- 要创建一个自身的静态私有成员变量;
- 要提供一个静态的公有工厂方法来返回单例
通过静态公有工厂方法创建单例虽然可以创建出唯一的单例对象,但是还是存在着问题:在多线程环境中,A线程来获取单例对象,判断instance==null为空,但还未创建成功单例对象时,另一个线程B也来获取单例对象,但这时A线程的单例对象还未创建完成,因此B线程也判断出instance == null,接下去也会创建一个新的单例对象。这就违背了单例模式的初衷:只能出现一个单例对象。对此,下面两种实现方式都可以解决这个问题。
2. 饿汉式单例
饿汉式单例,听着就让人感觉这个设计模式怎么这么饥渴呀?
没错,饿汉式单例就是“饥渴难耐”,不管你在程序中用不用,我先在定义静态变量的时候实例化单例类再说,因此在类加载的时候就创建了单例对象,看下代码:
class EagerSingleton{
//因为static,在类加载时,就已经创建单例对象
private static final EagerSingleton instance = new EagerSingleton();
//单例模式中构造方法都必须私有
private EagerSingleton(){}
public static EagerSingleton getInstance(){
return instance;
}
}
当类被加载时,静态变量instance会被初始化,此时类的私有构造函数会被调用,单例类的唯一实例将被创建,也就避免了在之后程序中因为多线程环境创建多个实例的问题。
懒汉式单例
有饿汉,那当然有懒汉了,那懒汉当然是“懒惰”的,只有在使用的时候,我才创建。那要怎么避免多线程中重复创建呢?看下代码:
class LazySingleton{
private static LazySingleton instance = null;
//单例模式中构造方法都必须私有
private LazySingleton(){}
synchronized public static LazySingleton getInstance(){
if (instance == null)
instance = new LazySingleton();
return instance;
}
}
其实懒汉式单例就在静态方法前面加了一个synchronized来同步操作。对上面的代码,我们还可以继续优化,因为我们每次判断只需要判断instance是否为空,所以其实无须对整个getInstance()方法进行锁定,只需对其中的代码”instance=new LazySingleton()”进行锁定即可。也就是:
public static LazySingleton getInstance(){
if (instance == null){
synchronized(LazySingleton.class){
instance = new LazySingleton();
}
}
return instance;
}
但是这样又出现问题了:假如在某一瞬间线程A和线程B都在调用getInstance()方法,此时Instance对象为null值,均能通过instance==null的判断,由于实现了synchronized加锁机制,线程A进入synchronized锁定的代码中执行实例创建代码,线程B处于排队等待状态,必须等待线程A执行完毕后才可以进入synchronized锁定代码。但A执行完毕时,线程B并不知道实例已经
创建,将继续创建新的实例,导致产生多个单例对象,违背单例模式的设计思想。所以还要进一步改进,在synchronized中再进行一次判断:
public static LazySingleton getInstance(){
//第一重判断
if (instance == null){
synchronized(LazySingleton.class){
//第二重判断
if (instance == null)
instance = new LazySingleton();
}
}
return instance;
}
。。。这尼玛,这两个老汉一个饿死一个懒死,饿汉不能实现延迟加载,不管将来用不用始终占据内存;懒汉线程安全控制烦琐,而且性能受影响,有没有一种方法可以避免上面两种资源的浪费呢?
IoDH技术
IoDH的全称叫做Initialization on Demand Holder,在单例类中增加一个静态内部类,在该内部类中创建单例对象,再将该单例对象通过getInstance()方法返回给外部使用,看代码:
class Singleton{
private Singleton(){}
private static class HolderClass{
private final static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return HolderClass.instance;
}
}
第一次调用getInstance()时将加载内部类HolderClass,在该内部类中定义了一个static类型的变量instance,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次,通过使用IoDH,既可以实现延迟加载,又可以保证线程安全,不影响系统性能,不失为一种最好的Java语言单例模式实现方式。
总结
饿汉式单例与懒汉式单例类比较:
-
饿汉式单例类在自己被加载时就将自己实例化。单从资源利用效率角度来讲,这个比懒汉式单例类稍差些。从速度和反应时间角度来讲,则比懒汉式单例类稍好些
-
懒汉式单例类在实例化时,必须处理好在多个线程同时首次引用此类时的访问限制问题,特别是当单例类作为资源控制器,在实例化时必然涉及资源初始化,而资源初始化很有可能耗费大量时间,这意味着出现多线程同时首次引用此类的机率变得较大,需要通过同步化机制进行控制。
- 单例模式的主要优点在于提供了对唯一实例的受控访问并可以节约系统资源;
- 其主要缺点在于因为缺少抽象层而难以扩展,且单例类职责过重。
- 单例模式适用情况包括:系统只需要一个实例对象;客户调用类的单个实例只允许使用一个公共访问点。