单例模式,你真的写对了吗? 面试必备!

单例模式应该是设计模式中最容易理解也是用得最多的一种模式了,同时也是面试的时候最常被问到的模式。

1. 单例模式的定义

单例模式指的是一个类中在任何情况下都绝对只有一个实例,并且提供一个全局访问点。

2. 单例模式的应用场景

单例模式的应用非常广泛,如数据库中的连接池、J2EE中的ServletContext和ServletContextConfig、Spring框架中的ApplicationContext等等。然而在Java中,单例模式还可以保证一个JVM中只存在一个唯一的实例。

单例模式的应用场景主要有以下几个方面:

  • 当需要频繁创建一些类的时候,使用单例可以降低系统的内存压力,减少GC(垃圾回收) ;
  • 当某些类创建实例时候需要占用的资源较多,或者实例化过程耗时比较长,且经常使用的情况;
  • 当存在频繁访问数据库或者文件的对象;
  • 当对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,是不允许存在多个实例的,否则玩完;

3. 单例模式的优缺点

3.1 单例模式的优点

  • 单例模式可以保证内存中只有一个实例对象,从而会减少内存的开销
  • 单例模式可以避免对资源的多重占用;
  • 单例模式设置全局访问点,可以起到优化和共享资源的访问的作用;

3.2 单例模式的缺点

  • 扩展难, 因为单例模式通常是没有接口的啊,如果想要扩展,那么你唯一途径就是修改之前的代码,所以说单例模式违背了开闭原则;
  • 调试难,因为在并发测试中,单例模式是不利于代码的调试的,单例中的代码没有执行完,也不能模拟生成一个新对象;
  • 违背单一职责原则,因为单例模式的业务代码通常写在一个类中,如果功能设计不合理,就很容易违背单一职责原则;

4. 单例模式的实现方式及其优缺点

4.1 单例模式的饿汉式实现

4.1.1 饿汉式标准写法

Singleton类称为单例类,通过内部初始化一次 , 隐藏构造方法, 并提供一个全局访问点的方式实现。

/**
 * msJava
 *
 * @Description 单例模式的通用写法
 * @Date 2021-01-23
 */
public class Singleton {
    /**
     * 内部初始化一次
     */
    private static final Singleton instance = new Singleton();

    /**
     * 隐藏构造方法
     */
    private Singleton() {
    }

    /**
     * 提供一个全局访问点
     *
     * @return Singleton
     */
    public static Singleton getInstance() {
        return instance;
    }

}

复制代码

以上饿汉式单例写法在类的初始化的时候就会进行初始化操作,并且创建对象,绝对的线程安全,因为此时线程还没有出现就已经实例化了,故不会存在访问安全的问题。

4.1.2 饿汉式静态块机制写法

饿汉式还有一种实现,那就是静态块机制,如下代码所示:

/**
 * msJava
 *
 * @Description 单例模式  饿汉式静态机制 实现
 * @Date 2021-01-23
 */
public class HungryStaticSingleton {
    
    private static final HungryStaticSingleton hungrySingleton;
    //静态代码块 类加载的时候就初始化
    static {
        hungrySingleton=new HungryStaticSingleton();
    }
    /**
     * 私有化构造函数
     */
    private HungryStaticSingleton(){}

    /**
     * 提供一个全局访问点
     * @return
     */
    public static HungryStaticSingleton getInstance() {
        return hungrySingleton;
    }
}
复制代码

我们分析一下这种是写法 ,可以明显的看到所以对象是类在加载的时候就进行实例化了,那么这样一来,会导致单例对象的数量不确定,从而会导致系统初始化的时候就造成大量内存浪费,况且你用不用还不一定,还一直占着空间,俗称“占着茅坑不拉屎”。

4.2 单例模式的懒汉式实现

为了解决饿汉式单例写法可能带来的内存浪费问题,这里分析一下懒汉式单例的写法。如下代码所示:

/**
 * msJava
 *
 * @Description 单例模式  懒汉式单例实现
 * @Date 2021-01-23
 */
public class LazySimpleSingleton {

    private static LazySimpleSingleton lazySingleton = null;

    /**
     * 私有化构造函数
     */
    private LazySimpleSingleton() {

    }
    /**
     * 提供一个全局访问点
     *
     * @return
     */
    public static LazySimpleSingleton getInstance() {
        if (lazySingleton == null) {
            lazySingleton = new LazySimpleSingleton();
        }
        return lazySingleton;
    }
}

复制代码

这样实现的好处就是只有对象被使用的时候才会进行初始化,不会存在内存浪费的问题,但是它会在多线程环境下,存在线程安全问题。我们可以利用synchronized关键字将全局访问点方法变成一个同步方法,这样就可以解决线程安全的问题,代码如下所示:

/**
 * msJava
 *
 * @Description 单例模式  懒汉式单例实现 synchronized修饰 
 * @Date 2021-01-23
 */
public class LazySimpleSingleton {
    private static LazySimpleSingleton lazySingleton = null;
    /**
     * 私有化构造函数
     */
    private LazySimpleSingleton() {}
    /**
     * 提供一个全局访问点  
     *
     * @return
     */
    public synchronized static  LazySimpleSingleton getInstance() {
        if (lazySingleton == null) {
            lazySingleton = new LazySimpleSingleton();
        }
        return lazySingleton;
    }
}
复制代码

但是,这样虽然解决了线程安全的问题,可是如果在线程数量剧增的情况下,用synchronized加锁,则会导致大批线程阻塞,从而骤减系统性能。

4.3 单例模式的双重检测实现

		在上述代码上进一步优化,代码如下所示:
复制代码

/**
 * msJava
 *
 * @Description 单例模式  懒汉式-双重检测单例实现
 * @Date 2021-01-23
 */
public class LazyDoubleCheckSingleton {
    // volatile 关键字修饰
    private volatile static LazyDoubleCheckSingleton lazySingleton ;
    /**
     * 私有化构造函数
     */
    private LazyDoubleCheckSingleton() {}
    /**
     * 提供一个全局访问点
     *
     * @return
     */
    public static LazyDoubleCheckSingleton getInstance() {
        // 这里先判断一下是否阻塞
        if (lazySingleton == null) {
            synchronized (LazyDoubleCheckSingleton.class){
                // 判断是否需要重新创建实例
                if (lazySingleton == null) {
                    lazySingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazySingleton;
    }
}
复制代码

当第一个线程调用getInstance()方法时,第二个线程也可以调用,但是第一个线程执行synchronized时候,第二个线程就会发现阻塞,但是此时的阻塞是getInstance()内部的阻塞。

4.4 单例模式的静态内部类实现

虽然双重检测锁的单例模式解决了线程安全和性能问题,但是毕竟涉及加锁的操作,多多少少就会到了性能的影响,下面我们分享一下更加优雅的单例模式实现,如下代码所示:

/**
 * msJava
 *
 * @Description 单例模式  静态内部类单例实现
 * @Date 2021-01-23
 */
public class LazyStaticInnerClassSingleton {
    //  在构造方法里面抛出异常真的合适?
  private LazyStaticInnerClassSingleton(){
    if(LazyHolder.INSTANCE != null){
        throw new RuntimeException("不允许创建多个实例");
    }
  }
  // static 保证这个方法不会被重写 覆盖
  private static LazyStaticInnerClassSingleton getInstance(){
      return LazyHolder.INSTANCE;
  }
  // Java 默认不会加载内部类
  private static class LazyHolder{
      private static final LazyStaticInnerClassSingleton INSTANCE=new LazyStaticInnerClassSingleton();
  }
}

复制代码

5. 总结

​ 单例模式面试几乎必备!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享