单例模式是我们最常见的设计模式之一。由于设计模式在面向对象程序设计中起着举足轻重的作用,在面试中很多面试官都喜欢问一些与设计模式相关的问题。在常用设计模式中,Singleton是唯一一个能用短短几十行代码完整实现的模式。因此,写一个Singleton的类型是一个很常见的面试题。
关于单例模式,网上有不少博客文章。我也看了不少,一开始似懂非懂,后来在《剑指offer》也看到单例模式,但是不是用java写的。所以自己也想用java总结一下java实现的单例模式。
题目:如何实现Singleton模式
题目:设计一个类,我们只能生成该类的一个实例。
不好的解法一:只适用于单线程环境(懒汉式,线程不安全)
由于要求只能生成一个实例,因此我们必须把构造函数设为私有函数以防止被多次创建实例。
我们可以声明一个静态(static)的实例,在需要的时候创建该实例,也就是实现懒加载。
下面的实现就是基于这个思路:
/**
* Created by Zheng548 on 2017/2/15 0015.
* 只适用于单线程环境(懒汉式,线程不安全)
* csdn blog :http://my.csdn.net/Zheng548?locationNum=0&fps=1
*/
public class Singleton1 {
private static Singleton1 instance = null;
private Singleton1() {
}
public static Singleton1 getInstance() {
if (instance == null) {
instance = new Singleton1();
}
return instance;
}
}
上面代码在Singleton1的静态属性instance中,只有在instance为null的时候才创建一个实例一避免重复创建。同时我们把构造函数定义为私有函数,这样才能确保只创建一个实例。
不好的解法二:能在多线程中工作但效率不高(懒汉式,线程安全)
解法一中的代码在单线程中工作正常,但在多线程的情况下就有问题了。设想两个线程同时运行到判断instance是否为null的if语句时,并且instance的确没有创建时,那么两个线程都会创建一个实例,此时Singleton就不能满足单例模式的要求了。
为了保证在多线程环境下我们还只能得到类的一个实例,需要加上一个同步锁。
/**
* Created by Zheng548 on 2017/2/15 0015.
* 能在多线程中工作但效率不高(懒汉式,线程安全)
* csdn blog :http://my.csdn.net/Zheng548?locationNum=0&fps=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方法上加了synchronized 关键字(了解更多关于synchronized ,请点击我),来实现同步。
下面在看看解法二有没有解法一的问题。
我们还是假设有两个线程同时想要创建一个实例。synchronized 方法是一种粗粒度的并发控制,某一时刻,只能有一个线程执行synchronized方法。当第一个线程获得锁时,第二个线程只能等待。于是,第一个线程发现此时实例还没有创建,它就创建一个实例,然后,它释放所获得的锁。此时第二个线程可以获得类的锁,发现实例已经被创建,于是它就不会重复创建实例。
这样就保证了我们在多线程环境中也只能得到一个实例。
然而,问题来了
虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。由于同步一个方法会降低100倍或更高的性能, 每次调用获取和释放锁的开销似乎是可以避免的:一旦初始化完成,获取和释放锁就显得很不必要。这就引出了双重检查锁定模式。
可行的解法三:双重检查锁定模式
什么是双重检查锁定模式,请看维基百科的权威解释:
双重检查锁定模式
其实,虽然这个名词听起来很厉害的样子,我这样的菜鸟为之一震,然而,它并不可怕。
首先,我们考虑一下,为什么要引入“双重检查锁定模式”这样一个概念呢?一般而言,引入新的概念通常是为了解决某个问题,那么我没呢的问题是什么,还记得吗?
第二,我们的问题。解法二的同步很低效,一旦初始化完成,instance创建之后,同步就显得不再必要。
第三,如何避免上述的问题呢?
1. 检查变量是否被初始化(不去获得锁),如果已被初始化立即返回这个变量。
2. 获取锁
3. 第二次检查变量是否已经被初始化:如果其他线程曾获取过锁,那么变量已被初始化,返回初始化的变量。
4. 否则,初始化并返回变量。
于是我们把解法二的代码做进一步的改进:
/**
* Created by Zheng548 on 2017/2/15 0015.
* 可行的解法三:双重检查锁定模式
* csdn blog :http://my.csdn.net/Zheng548?locationNum=0&fps=1
*/
public class Singleton3 {
private static Singleton3 instance = null;
private Singleton3() {
}
public static Singleton3 getInstance() {
if (instance == null) {
synchronized (Singleton3.class) {
if (instance == null) {
instance = new Singleton3();
}
}
}
return instance;
}
}
推荐解法一:利用静态构造函数
/**
* Created by Zheng548 on 2017/2/15 0015.
* 推荐解法一:利用静态构造函数
* csdn blog :http://my.csdn.net/Zheng548?locationNum=0&fps=1
*/
public class Singleton4 {
private static final Singleton4 instance = new Singleton4();
private Singleton4() {
}
public static Singleton4 getInstance() {
return instance;
}
}
这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。
缺点是它不是一种懒加载模式(lazy initialization),单例会在加载类后一开始就被初始化,即使客户端没有调用 getInstance()方法。
推荐解法二:静态内部类 实现单例模式
加载一个类时,其内部类是否同时被加载?引申出单例模式的另一种实现方式
加载一个类时,其内部类不会同时被加载。一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。
package com.company;
/**
* Created by Zheng548 on 2017/2/15 0015.
* 推荐解法二:静态内部类 实现单例模式
* csdn blog :http://my.csdn.net/Zheng548?locationNum=0&fps=1
*/
public class Singleton5 {
private static class SingletonHolder {
private static final Singleton5 INSTANCE = new Singleton5();
}
private Singleton5() {
}
public static final Singleton5 getInstance() {
return SingletonHolder.INSTANCE;
}
}
由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;