@一文搞懂设计模式--单例模式

前言

Hi,大家好呀。今天要说的是单例模式。单例模式的核心思想十分的简单,在开发一个系统的时候,为了保证系统中的数据一致,我们会希望某些特殊的类在整个系统的运行过程中只能存在一个实例。最简单的实现方式就是将这个要求交给开发者,让开发者在开发的时候多加注意。

当然,有时候我们不想要开发者人为的来维护这个实例,毕竟人总是会犯错的嘛,一不小心就会造成巨大的损失。因此,开发者们想出了一个设计模式来完成这件事情,这个设计模式就叫做单例模式。

单例模式的实现方式

单例的实现主要是通过以下几个步骤:

  1. 将类的构造方法定义为私有方法。这样其他类的代码就无法通过调用该类的构造方法来实例化该类的对象,只能通过该类提供的静态方法来得到该类的唯一实例。
  2. 定义一个私有的类的静态实例。
  3. 提供一个公有的获取实例的静态方法。

通常单例模式有以下几种具体实现方式:

  • 饿汉式实现
  • 懒汉式实现
  • 静态内部类实现

饿汉式会在类加载时就将单例对象创建。
懒汉式则是在使用类是才创建对象。

饿汉式的实现方式

public class Singleton {
    /**
     * 采用饿汉式,才类加载时创建实例
     */
    private static Singleton singleton = new Singleton();

    /**
     * 将构造函数私有化,禁止外部成员访问
     */
    private Singleton(){};

    /**
     * 外部成员只能通过getInstance获得实例
     * @return singleton
     */
    public static Singleton getInstance(){
        return singleton;
    }
}

这样一个简单的单实例对象就成功创建好了。采用饿汉式优点是线程安全、效率较高,但是由于饿汉式在类加载时就创建的对象,因此可能会有空间的浪费。在开发中可以使用,但是不推荐。

懒汉式实现方式

为了解决饿汉式浪费空间的问题,开发者有提出了懒汉式的实现方式,修改后的代码如下

public class Singleton {
    /**
     * 采用懒汉式,才类加载时创建实例
     */
    private static Singleton singleton = null;

    /**
     * 将构造函数私有化,禁止外部成员访问
     */
    private Singleton(){};

    /**
     * 外部成员只能通过getInstance获得实例
     * @return singleton
     */
    public static Singleton getInstance(){
        if(singleton==null){
            // ①
            singleton = new Singleton();
        }
        return singleton;
    }
}

这段代码在单线程的情况下没有问题,但是在并发时,如果在①处发生了线程切换,就将会产生并发问题,不能保证系统中只有一个实例。

对代码在此进行改进:

public class Singleton {
    /**
     * 采用懒汉式,才类加载时创建实例
     */
    private static Singleton singleton = null;

    /**
     * 将构造函数私有化,禁止外部成员访问
     */
    private Singleton(){};

    /**
     * 外部成员只能通过getInstance获得实例
     * @return singleton
     */
    public static Singleton getInstance(){
        if(singleton==null){
            synchronized (Singleton.class){
                if(singleton==null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

采用双重检查机制来避免出现并发情况下出现的问题。但是这样就ok吗。并没有,因为在new Singleton()对象时JVM可能发生指令重排。

什么是指令重排?
指令重排序是指源码顺序和程序顺序不一样,或者说程序顺序和执行的顺序不一致,例如在创建对象时吗,我们希望计算机是按照以下步骤运行

  1. 分配内存空间
  2. 执行构造方法,初始化对象
  3. 把这个对象指向这个空间

但是经过JVM指令重排后,实际上的运行过程可能是1、3、2。

如果在执行到3时发生了线程切换,新进来的线程就会认为singleton已经不为空了,直接将singleton返回,但是这时候的singleton并没有初始化。所以需要禁止指令重排。
添加volatile关键字防止指令重排

public class Singleton {
    /**
     * 采用懒汉式,才类加载时创建实例
     * 添加volatile关键字防止指令重排
     */
    private static volatile Singleton singleton = null;

    /**
     * 将构造函数私有化,禁止外部成员访问
     */
    private Singleton(){};

    /**
     * 外部成员只能通过getInstance获得实例
     * @return singleton
     */
    public static Singleton getInstance(){
        if(singleton==null){
            synchronized (Singleton.class){
                if(singleton==null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

采用懒汉式虽然解决了浪费空间的问题,但是在获取单例时需要进行加锁,因此效率较低,在开发中也不推荐使用。

静态内部类

那么在实际开发中应该怎么怎样实现单例模式。静态内部类是一种推荐的方法

public class Singleton {
    private static class SingletonHandler{
        private static Singleton singleton = new Singleton();
    }

    /**
     * 将构造函数私有化,禁止外部成员访问
     */
    private Singleton(){};

    /**
     * 外部成员只能通过getInstance获得实例
     * @return singleton
     */
    public static Singleton getInstance(){
        return SingletonHandler.singleton;
    }
}

JVM会推迟SingletonHandler的初始化操作,直到这个类被使用才会将其初始化,因此不需要同步。实际上,静态内部类中的互斥操作交给了JVM负责,不需要人为的显式同步。

优点:线程安全,懒加载,效率高

你可能感兴趣的:(java,面试典籍,设计模式,单例模式,设计模式,java,面试)