143、说出你所知道的所有单例设计模式写法

首先我们需要知道什么是单例设计模式,所谓单例设计模式,即某个类在整个系统当中只能有一个实例对象可以被获取和使用的编码模式。他是软件开发中最常使用的设计模式之一。

单例设计模式要点:

  1. 该类只能有一个实例:需把该类的构造器私有化;
  2. 它必须自行创建这个实例:定义一个该类的静态变量来保存这个唯一的实例
  3. 它必须向自行向整个系统提供这个实例:对外提供获取该实例对象的方式——要么直接暴露该实例对象,要么用静态的get方式获取

几种常见形式:

  1. 饿汉式:直接创建对象,不存在线程安全问题;(又可分为直接实例化饿汉式、枚举式、静态代码块饿汉式)
  2. 懒汉式:延迟创建对象(又可分为线程安全写法和线程不安全写法,还有静态内部类形式写法)

先看第一种写法——饿汉式的直接实例化形式:

/**
 * @author YuZhansheng
 * @desc  懒汉式:直接创建实例对象,不管你需不需要这个对象
 *        1、构造器私有化
 *        2、自行创建,并且使用静态变量保存
 *        3、向外提供这个实例
 *        4、为了强调这是一个单例,我们使用final修饰这个实例
 * @create 2019-03-17 11:39
 */
public class Singleton1 {
    public static final Singleton1 INSTANCE = new Singleton1();
    private Singleton1(){}
}

JDK1.5之后对上面这种形式提供了更加简洁的写法,那就是使用枚举来实现单例模式,看第二种写法:

/**
 * @author YuZhansheng
 * @desc   枚举类型表示该类型的对象是有限的几个。
 *         在这里我们限定该类型的对象就为一个,就成了单例。
 *         枚举类型的构造器都是私有化的
 * @create 2019-03-17 11:47
 */
public enum Singleton2 {
    INSTANCE
}

下面测试一下上面两种写法的正确性:

public class TestSingleton {
    public static void main(String[] args) {
        Singleton1 s1 = Singleton1.INSTANCE;
        Singleton1 s2 = Singleton1.INSTANCE;
        System.out.println(s1 == s2);

        Singleton2 s3 = Singleton2.INSTANCE;
        Singleton2 s4 = Singleton2.INSTANCE;
        System.out.println(s3 == s4);
    }
}

两个输出都是true。(看着上面这个测试怎么那么傻呢。。。)

下面看单例设计模式的第三种写法:静态代码块饿汉式

public class Singleton3 {
    public static final Singleton3 INSTANCE;
    static {
        INSTANCE = new Singleton3();
    }
    private Singleton3(){}
}

Singleton1和Singleton3的执行效果是一样的,都是在类加载和初始化的时候静态常量和静态代码块就直接被创建出来了。那么什么时候会用到Singleton3这种情况呢?适合于需要引入配置文件的时候,需要事先读取一堆配置文件才可以把对象创建成功的情形,需要把加载配置文件的程序也放在静态代码块里面。如下示例的单例程序:

import java.io.IOException;
import java.util.Properties;

public class Singleton3 {
    public static final Singleton3 INSTANCE;
    private String info;
    static {
        Properties pro = new Properties();
        try {
            pro.load(Singleton3.class.getClassLoader().getResourceAsStream("single.properties"));
            INSTANCE = new Singleton3(pro.getProperty("info"));
        } catch (IOException e) {
            throw  new RuntimeException(e);
        }
    }
    private Singleton3(String info){
        this.info = info;
    }

    public String getInfo() {
        return info;
    }

    public void setInfo(String info) {
        this.info = info;
    }

    @Override
    public String toString() {
        return "Singleton3{" +
                "info='" + info + '\'' +
                '}';
    }
}

测试上面的单例:single.properties在resources目录下,文件就只有两行:

#key=value
info = xidian
public class TestSingleton {
    public static void main(String[] args) {
        Singleton3 s = Singleton3.INSTANCE;
        System.out.println(s);
    }
}

打印输出:Singleton3{info='xidian'}

饿汉式的三种创建方式已经讲解完毕,此三种方式共同的特点就是在类初始化的时候就new出了对象,并且饿汉式不存在线程安全问题,下面开始讲解懒汉式。

饿汉式的特点就是无论你需要不需要,在类初始化的时候就将对象创建出来,可是有的时候我们不需要这个对象啊?因此就出现了懒汉式,所谓懒汉式,就是在类初始化的时候不创建对象,当你需要使用对象的时候再创建。

/**
 * @author YuZhansheng
 * @desc   懒汉式:延迟创建这个对象
 *         1、构造器私有化
 *         2、用一个静态变量保存这个唯一实例,注意,和Singleton1不同的是,此静态变量要是private,外部不能使用
 *         3、提供一个静态方法,用来获取这个对象的实例
 * @create 2019-03-17 15:25
 */
public class Singleton4 {
    private static Singleton4 instance;
    private Singleton4(){}
    public static Singleton4 getInstance(){
        if (instance == null){
            instance = new Singleton4();
        }
        return instance;
    }
}

上面这种方式是线程不安全的,在单线程环境下使用没有问题。为什么是线程不安全呢?这里简单说一下,假设同时存在两个线程要创建实例,就是执行getInsance()方法啊,线程A判断一下instance是否为空,发现为空,在线程A还没有来得及new Singleton4()的时候,这时候线程B也进来判断了,发现也是空,于是,线程B也去执行new Singleton4()了,这就创建了两个不同的对象。下面我们就使用同步来改造Singleton4,使之成为线程安全的:

/**
 * @author YuZhansheng
 * @desc   懒汉式:延迟创建这个对象
 *         1、构造器私有化
 *         2、用一个静态变量保存这个唯一实例,注意,和Singleton1不同的是,此静态变量要是private,外部不能使用
 *         3、提供一个静态方法,用来获取这个对象的实例,添加同步锁机制,保证线程安全
 * @create 2019-03-17 15:25
 */
public class Singleton5 {
    private static Singleton5 instance;
    private Singleton5(){}
    public static Singleton5 getInstance(){
        synchronized (Singleton5.class){
            if (instance == null){
                instance = new Singleton5();
            }
        }
        return instance;
    }
}

这种还不是最优版,上面这一版还有一个问题,那就是无论对象创建还是未创建都会先进入到那个同步代码块,然后判断是否为空,如果对象已经创建过了,本来可以直接return instance就行了,还要先执行同步代码块的程序,这就造成性能降到,因此改进Singleton5,先进行一次是否为空判断,如果不为空,说明之前已经创建过对象,直接返回,跨过同步代码块那些执行语句,如果为空,说明还未创建,再进入到同步代码块中创建实例对象。如下:

public class Singleton5 {
    private static Singleton5 instance;
    private Singleton5(){}
    public static Singleton5 getInstance(){
        if (instance == null){
            synchronized (Singleton5.class){
                if (instance == null){
                    instance = new Singleton5();
                }
            }
        }
        return instance;
    }
}

上面的代码虽然满足了单例设计模式,但是看起来有点复杂,下面看单例设计模式的第六种写法:

public class Singleton6 {
    private Singleton6(){}
    private static class Inner{
        private static final Singleton6 INSTANCE = new Singleton6();
    }
    public static Singleton6 getInstance(){
        return Inner.INSTANCE;
    }
}

上面的代码只有在静态内部类被加载和初始化的时候,才会创建new Singleton6()创建实例对象,而当我们调用getInstance()方法的时候,才会调用静态内部类Inner,如果没有调用getInstance(),就不会加载和初始化Inner类。注意:静态内部类不会自动随着外部类的加载和初始化而加载和初始化的,它是独立的一个类,它是要单独加载和初始化的。由于是在静态内部类里面加载和初始Singleton6的,所以也是线程安全的。比第五种看起来更加简洁一点。

至此,六种常用的单例设计模式已经讲解完毕,有什么疑问欢迎下方留言噢。