Java详解剑指offer面试题2-单例模式
1 题目描述
单例模式需要满足如下规则:
- 构造函数私有化(private),使得不能直接通过new的方式创建实例对象;
- 通过new在代码内部创建一个(唯一)的实例对象;
- 定义一个public static的公有静态方法,返回上一步中创建的实例对象;由于在静态方法中,所以上一步的对象也应该是static的。
2 代码实现
2.1 饿汉模式
根据这个规则,我们可以写出如下模式,这种模式又被称为饿汉模式。不管用不用得到,先new出来再说。
/** 单例模式,饿汉模式,不管为不为空,先直接new一个出来 */
public class EagerSingleton {
private static volatile EagerSingleton instance = new EagerSingleton();
// private constructor,私有化该类的构造函数
private EagerSingleton() {
}
public static EagerSingleton getInstance() {
return instance;
}
}
这是实现一个安全的单例模式的最简单粗暴的写法,这种实现方式我们称之为饿汉式。之所以称之为饿汉式,是因为肚子很饿了,想马上吃到东西,不想等待生产时间。这种写法,在类被加载的时候就把Singleton实例给创建出来了。
饿汉式的缺点就是,可能在还不需要此实例的时候就已经把实例创建出来了,没起到lazy loading的效果。优点就是实现简单,而且安全可靠。
2.2 懒汉模式
和饿汉模式对应的称为懒汉模式,实例为空时才new出来。
/**
* 单例模式,懒汉模式,为空才new */
public class LazyInitializedSingleton {
private static LazyInitializedSingleton instance;
private LazyInitializedSingleton(){}
public static LazyInitializedSingleton getInstance(){
if(instance == null){
instance = new LazyInitializedSingleton();
}
return instance;
}
}
相比饿汉式,懒汉式显得没那么“饿”,在真正需要的时候再去创建实例。在getInstance方法中,先判断实例是否为空再决定是否去创建实例,看起来似乎很完美,但是存在线程安全问题。在并发获取实例的时候,可能会存在构建了多个实例的情况。所以,在2.3线程安全的懒汉模式
对此代码进行了改进。
2.3 线程安全的懒汉模式
懒汉模式在单线程下可以很好地工作,但是如果多个线程同时执行到if (instance == null)
这句判空操作,那么将会同时创建多个实例对象,所以为了保证在多线程下实例只被创建一次,需要加同步锁。这就是线程安全的懒汉模式,虽然它能在多线程下工作,但效率不高。
/** LazyInitializedSingleton在多线程中,如果多个线程同时运行到if (instance == null) 就会创建多个对象,所以加上同步锁 */
public class ThreadSafeSingleton {
private static ThreadSafeSingleton instance;
private ThreadSafeSingleton(){}
public static ThreadSafeSingleton getInstance(){
if(instance == null){
synchronized (ThreadSafeSingleton.class) {
instance = new ThreadSafeSingleton();
}
}
return instance;
}
}
2.4 双重校验锁法
上面的代码在每次调用方法时候都会加锁(即使实例早已被创建),我们知道加锁是很耗时的,实际上我们主要是为了保证在对象为null时,只new出一个实例,只在这个时候加锁就够了。基于这点,改进如下。在下面的双重校验锁法中,同步锁只在实例第一次被创建时候才加上。这里还用到了volatile关键字来修饰singleton,其最关键的作用是防止指令重排。
/** ThreadSafeSingleton中每次调用getInstance()方法都会加同步锁,而加锁是一个很耗时的过程,实际上加锁只需要在第一次创建对象时 */
public class DoubleCheckedLockingSingleton {
private volatile static DoubleCheckedLockingSingleton instance;
private DoubleCheckedLockingSingleton() {}
public static DoubleCheckedLockingSingleton getInstance() {
// 第一次创建时才加锁
if (instance == null) {
synchronized (DoubleCheckedLockingSingleton.class) {
if (instance == null) {
instance = new DoubleCheckedLockingSingleton();
}
}
}
return instance;
}
}
2.5 静态代码块法
我们知道在Java中,静态代码块只会在用到该类的时候(类加载,调用了静态方法等)被调用唯一的一次,因此在静态代码块中创建实例对象是个不错的选择。
/** 静态代码块只在类加载的时候调用一次(静态方法调用等第一次用到该类的时候) */
public class StaticBlockSingleton {
private static StaticBlockSingleton instance;
private StaticBlockSingleton() {}
static {
instance = new StaticBlockSingleton();
}
public static StaticBlockSingleton getInstance() {
return instance;
}
//如果调用该类的任意静态方法,都会创建该类的实例,导致过早创建
public static void func() {}
}
但是,我们也注意到,如果我们调用StaticBlockSingleton类的其他静态方法,例如func()
静态方法,这就会导致StaticBlockSingleton类的实例被过早的创建,而这不是我们希望看到的。
2.6 静态类内部加载法
Bill Pugh是Java内存模型更改背后的主要推手,而他建议使用静态内部类来创建单例。因为使用静态内部类的好处是:静态内部类不会在单例加载时就加载,而是在调用getInstance()
方法时才进行加载,达到了类似懒汉模式的效果,而这种方法又是线程安全的。
public class StaticInnerClassSingleton {
private static class SingletonHolder{
private static StaticInnerClassSingleton instance=new StaticInnerClassSingleton();
}
private StaticInnerClassSingleton(){
System.out.println("Singleton has loaded");
}
public static StaticInnerClassSingleton getInstance(){
return SingletonHolder.instance;
}
}
似乎静态内部类看起来已经是最完美的方法了,其实不是,可能还存在反射攻击或者反序列化攻击。且看如下代码:
public static void main(String[] args) throws Exception {
Singleton singleton = Singleton.getInstance();
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton newSingleton = constructor.newInstance();
System.out.println(singleton == newSingleton);
}
上述代码的运行结果:
通过结果看,这两个实例不是同一个,这就违背了单例模式的原则了。
除了反射攻击之外,还可能存在反序列化攻击的情况。例如引入依赖:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
这个依赖提供了序列化和反序列化工具类。
Singleton类实现java.io.Serializable接口,如下:
public class Singleton implements Serializable {
private static class SingletonHolder {
//该语句只在类加载的初始化阶段才会执行,并且只执行一次
private static Singleton instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
byte[] serialize = SerializationUtils.serialize(instance);
Singleton newInstance = SerializationUtils.deserialize(serialize);
System.out.println(instance == newInstance);
}
}
上述代码的运行结果:
2.7 枚举模式
《Effective Java》作者Josh Bloch 提倡用枚举模式来实现单例模式,这种写法解决了以下三个问题:自由序列化、保证只有一个实例、线程安全。最佳的单例实现模式就是枚举模式。利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。其写法如下:
public enum EnumSingleton {
INSTANCE;
public void doSomething() {
System.out.println("doSomething");
}
}
如果我们想调用它的方法时,仅需要以下操作:
public class Main {
public static void main(String[] args) {
EnumSingleton.INSTANCE.doSomething();
}
}
直接通过EnumSingleton.INSTANCE.doSomething()
的方式调用即可。方便、简洁又安全。
本文参考文献:
[1]Java单例模式:为什么我强烈推荐你用枚举来实现单例模式
[2]剑指offer-面试题2(实现单例模式)
[3]github.com/haiyusun/data-structures
[4]All About the Singleton