Java详解剑指offer面试题2-单例模式(史上最全的Java单例模式和原理解析)

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

快乐李同学(李俊德-大连理工大学)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值