谁要是再问你单例模式,那就抛给他这7种写法吧!

来自:无敌码农,作者:无敌码农

单例设计模式是23种设计模式中,最基础也是最常用的设计模式之一,也是面试中关于设计模式知识点考察比较高频的问题之一。说起单例模式的写法,大多数情况下出现在我们脑海中的可能就是“饿汉式”,“懒汉式”这两种写法,但是今天小码哥今天要介绍的是单例模式的7种写法,以后面试官要是再问你单例模式,那就抛给他这七种写法吧!


接下来,我们就言归正传,来一一介绍这七种单例模式的写法吧!

1、饿汉式

饿汉式是单例模式设计中比较经典的实现方式。实现代码如下:

//final不允许被继承
public final class SingleTonEhangshi {
    //实例变量
    private byte[] data = new byte[1024];

    //在定义实例对象时直接初始化
    private static SingleTonEhangshi instance = new SingleTonEhangshi();

    //私有化构造函数,不允许外部NEW
    private SingleTonEhangshi() {

    }

    public static SingleTonEhangshi getInstance() {
        return instance;
    }
}


饿汉式的实现关键在于instance作为类变量直接得到了初始化,如果我们主动使用SingleToEhangshi类,那么instance实例将会直接完成创建,包括其中的实例变量也都会得到初始化。


instance作为类变量,在类初始化的过程中会被收集进<clinit>()方法中,而该方法是可以100%地保证同步,也就是说instance在多线程的情况下不可能被初始化两次。但是由于instance被ClassLoader加载后很长一段时间才被使用的话,那就会意味着instance实例所开辟的堆内存会驻留很长的时间。


总体说来,如果一个类中的成员变量比较少,且占用的内存资源也不多,用饿汉式的方式实现单例模式也未尝不可,只是其无法进行懒加载。

2、懒汉式

所谓懒汉式就是在使用类实例的时候再去创建,也就是说用到的时候我再创建,这样就可以避免类在初始化的时候提前创建过早地占用内存空间。实现代码如下:

//final不允许被继承
public final class SingleTonLhangshi {
    //实例变量
    private byte[] data = new byte[1024];

    //定义实例,但是不直接初始化
    private static SingleTonLhangshi instance = null;

    //私有化构造函数,不允许外部NEW
    private SingleTonLhangshi() {

    }

    public static SingleTonLhangshi getInstance() {
        if (null == instance) {
            instance = new SingleTonLhangshi();
        }
        return instance;
    }
}


类变量instance=null,因此当类被初始化的时候instance并不会立刻被实例化,而是在getInstance()方法被调用时判断instance实例是否被实例化,如果没有实例化在调用私有构造方法进行实例化操作。


懒汉式写法在多线程环境下,会存在同一时间多个线程同时看到null==instance的情况,从而导致instance会被实例化多次,从而无法保证单例的唯一性。

3、懒汉式+同步方法

懒汉式的单例实现方式可以保证实例的懒加载,但是却无法保证实例的唯一性。在多线程环境下由于instance为共享数据,当多个线程访问使用时,需要保证数据的同步性,所以如果需要保证懒汉式实例的唯一性,我们可以通过同步的方式来实现。代码如下:

/final不允许被继承
public final class SingleTonLhangshiSync {
    //实例变量
    private byte[] data = new byte[1024];

    //定义实例,但是不直接初始化
    private static SingleTonLhangshiSync instance = null;

    //私有化构造函数,不允许外部NEW
    private SingleTonLhangshiSync() {

    }

    //向getInstance方法加入同步控制,每次只能有一个线程能够进入
    public static synchronized SingleTonLhangshiSync getInstance() {
        if (null == instance) {
            instance = new SingleTonLhangshiSync();
        }
        return instance;
    }
}


采用懒汉式+数据同步的方法既满足了懒加载又能够100%保证instance实例的唯一性。但是,synchronized关键字的排它性会导致getInstance()方法同一时刻只能被一个线程访问,性能会比较低下。

4、Double-Check

Double-Check是一种比较聪明的设计方式,它提供了一种高效的数据同步策略,那就是首次初始化的时候加锁,之后则允许多个线程同时进行getInstance()方法的调用来获得类的实例。代码如下:

//final不允许被继承
public final class SingletonDoubleCheck {
    //实例变量
    private byte[] data = new byte[1024];

    //定义实例,但是不直接初始化
    private static SingletonDoubleCheck instance = null;

    Connection con;
    Socket socket;

    //私有化构造函数,不允许外部NEW
    private SingletonDoubleCheck() {
        this.con = con;//初始化
        this.socket = socket;//初始化

    }
    public static SingletonDoubleCheck getInstance() {
        //当instance为null时,进入同步代码块,同时该判断避免了每次都需要进入同步代码块,可以提高效率
        if (null == instance) {
            //只有一个线程能够获得SingletonDoubleCheck.class关联的monitor
            synchronized (SingletonDoubleCheck.class) {
                //判断如果instance为null则创建
                if (null == instance) {
                    instance = new SingletonDoubleCheck();
                }
            }
        }
        return instance;
    }
}


当两个线程发现null==instance成立时,只有一个线程有资格进入同步代码块,完成对instance的初始化,随后的线程发现null==instance不成立则无须进行任何操作,以后对getInstance的访问就不会再需要进行数据同步了。


此种方式看起来是既满足了懒加载,又保证了instance实例的唯一性,并且还提供了比较高效的数据同步策略,可以允许多个线程同时对getInstance进行访问。但是这种方式在多线程的情况下,可能会引起空指针异常,这是因为如果在如上代码的构造方法中还存在初始化其他资源的情况的话,由于JVM运行时存在指令重排的情况,这些资源在实例化时顺序并无前后关系的约束,那么在这种情况下,就极有可能是instance最先被实例化,而con和socket并未完成实例化,而未完成实例化的实例在调用其方法时将会抛出空指针异常。

5、Volatile+Double-Check

为了解决Double-Check指令重排导致的空指针问题,可以用volatile关键字防止这种重排序的发生。因此代码只需要稍作修改就能满足多线程下的单例、懒加载以及实例的高效性了。代码如下:

//final不允许被继承
public final class SingletonDoubleCheck {
    //实例变量
    private byte[] data = new byte[1024];

    //定义实例,但是不直接初始化
    private static volatile SingletonDoubleCheck instance = null;

    Connection con;
    Socket socket;

    //私有化构造函数,不允许外部NEW
    private SingletonDoubleCheck() {
        this.con = con;//初始化
        this.socket = socket;//初始化

    }

    public static SingletonDoubleCheck getInstance() {
        //当instance为null时,进入同步代码块,同时该判断避免了每次都需要进入同步代码块,可以提高效率
        if (null == instance) {
            //只有一个线程能够获得SingletonDoubleCheck.class关联的monitor
            synchronized (SingletonDoubleCheck.class) {
                //判断如果instance为null则创建
                if (null == instance) {
                    instance = new SingletonDoubleCheck();
                }
            }
        }
        return instance;
    }
}

6、Holder方式

Holder方式完全借助了类加载的特点。代码如下:

//不允许被继承
public final class SingletonHolder {
    //实例变量
    private byte[] data = new byte[1024];

    private SingletonHolder() {

    }

    //在静态内部类中持有单例类的实例,并且可直接被初始化
    private static class Holder {
        private static SingletonHolder instance = new SingletonHolder();
    }

    //调用getInstance方法,事实上是获得Holder的instance静态属性
    public static SingletonHolder getInstance() {
        return Holder.instance;
    }
}


在单例类中并没有instance的静态成员,而是将其放到了静态内部类Holder之中,因此单例类在初始化的过程中并不会创建SingletonHolder的实例,内部类Holder中定义了SingletonHolder的静态变量,并且直接进行了实例化,只有当Holder被主动引用的时候才会创建SingletonHolder的实例。


SingletonHolder实例的创建过程在Java程序编译时期收集至<clinit>()方法中,该方法又是同步方法,可以保证内存的可见性、JVM指令的顺序性和原子性。Holder方式的单例模式设计是最好的设计之一,也是目前使用比较广的设计。

7、枚举方式

枚举方式在很多开源框架中也应用得比较广泛,枚举类型不允许被继承,同样是线程安全的,并且只能被实例化一次,但是枚举类型不能够实现懒加载。用枚举类型,实现单例模式的代码如下:

public class SingletonEnum {
    //实例变量
    private byte[] data = new byte[1024];

    private SingletonEnum() {

    }

    //使用枚举充当Holder
    private enum EnumHolder {
        INSTANCE;
        private SingletonEnum instance;

        EnumHolder() {
            this.instance = new SingletonEnum();
        }

        private SingletonEnum getInstance() {
            return instance;
        }
    }

    public static SingletonEnum getInstance() {
        return EnumHolder.INSTANCE.getInstance();
    }
}


以上就是要给大家介绍的单例模式的7种写法了,虽然单例模式非常简单,但是在多线程的情况下,我们之前所设计的单例程序未必能够满足单实例、懒加载以及高性能的特点。

推荐↓↓↓
Java编程
上一篇:如何彻底理解volatile关键字? 下一篇:线程池的设计原理是什么?