本文目录:
题目:设计一个类,我们只能生成该类的一个实例。
考点:对单例模式(Singleton)模型的理解。
只能生成一个实例的类是实现了Singleton(单例)模式的类型。
开篇我们先说明一个问题:“懒汉式和饿汉式”的是什么意思?
懒汉式和饿汉式
所谓“懒汉式”与“饿汉式”的区别就是:在建立单例对象的时间的不同。
懒汉式:在你真正用到的时候才去创建这个单例对象
public class Singleton(){
private static Singleton singleton = null; // 只是声明一个singleton对象,并没有创建
private Singleton(){
}
public static synchronized Singleton getInstance(){
if(singleton == null){ // 先判断是否为空
singleton = new Singleton(); // 懒汉式做法
}
return singleton;
}
}
饿汉式:不管你会不会用上,一开始就创建这个单例对象
public class Singleton(){
private static Singleton singleton = new Singleton(); // 只是声明一个singleton对象,并没有创建
private Singleton(){
}
public static synchronized Singleton getInstance(){
return singleton; // 直接返回创建好的singleton对象
}
}
解法1:只适用于单线程环境 (不好)
由于要求只能生成一个实例,因此我们必须把构造函数设为私有函数已禁止他人创建实例。我们可以定义一个静态实例,在需要的时候创建该实例。代码如下所示:
public class Singleton1 {
private static Singleton1 instance = null;
// 私有的构造函数
private Singleton1(){
}
public static Singleton1 getInstance(){
if(instance == null){
instance = new Singleton1();
}
return instance;
}
}
Singleton的静态属性instance,只有instance为null的时候才创建一个实例,构造函数私有,确保每次都只创建一个,避免重复创建。
缺点:只在单线程的情况下正常运行,在多线程的情况下,就会出问题。例如:当两个线程同时运行到判断instance是否为空的 if 语句,并且instance确实没有创建好时,那么两个线程都会创建一个实例,此时类型Singleton1就不满足单例模式的要求了。
解法2:虽然在多线程环境中能工作,但是效率不高 (不好)
在解法1的基础上加上了同步锁,使得在多线程的情况下可以用。
public class Singleton2 {
private static Singleton2 instance = null;
// 私有的构造函数
private Singleton2(){
}
// 同步方法
public static synchronized Singleton2 getInstance(){
if(instance == null){
instance = new Singleton2();
}
return instance;
}
}
当两个线程同时想创建一个实例,由于在同一个时刻只有一个线程能得到同步锁,当第一个线程加上锁以后,第二个线程只能等待。当第一个线程发现实例还没有创建,则创建。接着第一个线程释放同步锁,第二个线程才可以加上同步锁,执行下面的代码。由于第一个线程已经创建了实例,所以第二个线程不需要创建实例。保证在多线程的环境下也只有一个实例。
缺点:每次通过 getInstance 方法得到 singleton 实例的时候都有一个试图去获取同步锁的过程。而众所周知,加锁是很耗时的,这也是这种方法效率低下的主要原因,在没有必要的时候要尽量避免。
解法3:加同步锁前后两次判断实例是否存在 (可行)
我们只是在还没有创建之前需要加锁操作,以保证一个线程创建出实例。而当实例已经创建之后,我们就不需要再执行加锁操作了。
public class Singleton3 {
private static Singleton3 instance = null;
// 私有的构造函数
private Singleton3(){
}
// 两次 if 判断
public static Singleton3 getInstance(){
if(instance == null){
// 若instance为null,说明它还未创建,则进行加锁操作
synchronized (Singleton3.class) {
if(instance == null){
instance = new Singleton3();
}
}
}
return instance;
}
}
Singleton3 中只有当 instance 为 null 即没有创建时,需要进行加锁操作。当 instance 已经创建出来之后,则无需再进行加锁。因为只在第一次的时候 instance 为 null,因此只在第一次试图创建实例的时候需要加锁。这样,Singleton3 的时间效率比 Singleton2 要好的多。
Singleton3 用加锁机制来确保在多线程环境下创建一个实例,并且用两个 if 判断来提高效率。这样的代码实现起来比较复杂,容易出错,下面看两种比较优秀的解法。
解法4:利用类静态变量初始化一个实例 (推荐使用)
其实就是饿汉式,在初始化静态变量 instance 的时候创建一个实例。
public class Singleton4(){
private static Singleton4 singleton = new Singleton4(); // 只是声明一个singleton对象,并没有创建
private Singleton4(){
}
public static synchronized Singleton4 getInstance(){
return singleton; // 直接返回创建好的singleton对象
}
}
初试化静态的 instance 创建一次。如果我们在Singleton4类里面写一个静态的方法不需要创建实例,它仍然会早早的创建一次实例。而降低内存的使用率。
缺点:没有懒加载的效果,从而降低内存的使用率。
解法5:静态内部类实现按需创建实例 (推荐使用)
public class Singleton5 {
// 私有的构造函数
private Singleton5(){
}
// 私有的内部类
private static class SingletonHolder{
private final static Singleton5 instance = new Singleton5();
}
public static Singleton5 getInstance(){
return SingletonHolder.instance; // 返回SingletonHolder中创建的实例
}
}
注解:定义一个私有的内部类,在第一次用这个内部类时,会创建一个Singleton5实例。而类型为SingletonHolder的类,只有在Singleton5.getInstance()中调用,由于私有的属性,他人无法使用SingletonHolder,所以即便调用Singleton.getInstance()也不会创建实例。
优点:达到了懒加载的效果,即按需创建实例。
总结:
第1种方法:多线程环境下不能工作;
第2种方法:虽然能够在多线程环境下工作,但是加锁耗时,效率不高;
第3种方法:通过两次 if 判断实现只有 instance为 null 时才加锁,实现多线程下高效的工作,但是代码逻辑相对复杂点;
第4种方法:通过直接创建出一个实例给类的静态变量,即”饿汉式“;
第5种方法:通过私有的内部类方法完成按需创建。
面试中推荐后第4种或者第5种解法。