本篇博客主要记录一下单例模式。
单例模式大家应该都不陌生,主要用于创建一个“独一无二”的对象。
它基本的写法类似于:
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
容易看出,单例模式其实就是利用一个静态变量uniqueInstance来记录Singleton的唯一实例。
Singleton的构造器被声明为私有的,因此只有内部方法可以调用。
当客户端代码试图获取单例对象时,必须调用其静态方法getInstance。
在getInstance方法中,将判断Singleton是否被实例化过。
如果实例化过,则直接返回uniqueInstance;否则,将先实例化单例对象。
通过这种方式,外部类不能随意地实例化Singleton;
仅能通过Singleton提供的接口,获取对象。
而Singleton又管制了自身的创建,于是最终保证Singleton“独一无二”。
单例模式的定义和结构图都很简单,如下所示:
单例模式确保一个类只有一个实例,并提供一个全局访问点。
如图所示,单例模式其实就只有一个类,
它主要是通过上文中代码的固定“套路”来保证对象的“独一无二”。
需要注意的是,在多线程的场景下,上述单例的实现方式,将会产生问题。
例如,在初次初始化时,两个线程同时进入到getInstance方法,
并判断uniqueInstance为null,此时将会初始化多个实例。
针对这个问题,在多线程场景下,主要有三种解决方案。
1、同步getInstance方法
按照这种方案修改后的代码类似于:
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {}
//增加synchronized关键字
//迫使每个线程在进入该方法前,必须等待其它线程离开
public static synchronized Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
这种修改方案比较简单,唯一的缺点是对性能有影响。
如果getInstance将被频繁调用,那么就不能这么改。
2、由JVM初始化类时创建单例
按照这种方案修改后的代码类似于:
public class Singleton {
//类被加载时,就创建出单例
//保证了线程安全
private static Singleton uniqueInstance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return uniqueInstance;
}
}
一般单例被频繁使用,或创建和运行的负担不重时,可以采取这种方式。
3、利用“double checked locking”来同步getInstance方法
public class Singleton {
//这里注意使用volatile关键字
//保证一个线程创建单例后,另一个线程能够发现这种改变
private volatile static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getInstance() {
if (uniqueInstance == null) {
//只有第一次创建时,才会真正利用synchronized进行同步
//因此对性能的影响较小
synchronized (Singleton.class) {
//进入同步块后,再次判断是否创建
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
这种方式可以看作对第1种方式的性能优化。
最后,需要说明的是:
每个类加载器都定义了一个命名空间,如果有两个以上的类加载器,
不同的类加载器可能会加载同一个类,从整个程序来看,同一个类会被加载多次。
如果这种事情发生在单例上,就会产生多个单例并存的现象。
因此,如果程序中有多个类加载器又使用了单例模式,
那么最后指定同一个类加载器加载单例类。