Java设计模式:创建型模式(一)——单例模式

520 阅读12分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

自Java语言推广使用以来,单例模式就是最常用的设计模式,它具有易于理解、使用简便的特点。有时单例模式会过度使用或在不合适的场景下使用,造成利大于弊的后果,因此,单例模式有时候会被认为是一种反模式,但是很多情况下,单例模式还是不可或缺的。

单例模式,顾名思义,用来保证一个对象只能创建一个实例,除此之外,他还提供了对实例全局访问方法。

1. 单例模式的结构

单例模式的主要有以下角色:

  • 单例类,只能创建一个实例的类
  • 访问类,使用单例类

2. 单例模式的实现

单例设计模式分类两种:

  • 饿汉式:类加载就会导致该单实例对象被创建
  • 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
  • 枚举方式

2.1 饿汉式

方式1(静态变量方式)

/**
 * 饿汉式-静态变量创建类的对象
 */
public class Singleton {
    // 私有构造方法
    private Singleton() {}

    // 在成员位置创建该类的对象
    private static Singleton instance = new Singleton();

    // 对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return instance;
    }
}

该方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。

// 创建Singleton类的对象
Singleton instance01 = Singleton.getInstance();
Singleton instance02 = Singleton.getInstance();

System.out.println(instance01 == instance02);

// 执行结果
true

方式2(静态代码块方式)

/**
 * 饿汉式-在静态代码块中创建该类对象
 */
public class Singleton {

    //私有构造方法
    private Singleton() {}

    //在成员位置创建该类的对象
    private static Singleton instance;

    static {
        instance = new Singleton();
    }

    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return instance;
    }
}

该方式在成员位置声明Singleton类型的静态变量,而对象的创建是在静态代码块中,也是对着类的加载而创建。所以和饿汉式的方式1基本上一样,当然该方式也存在内存浪费问题。

// 创建Singleton类的对象
Singleton instance01 = Singleton.getInstance();
Singleton instance02 = Singleton.getInstance();

System.out.println(instance01 == instance02);

//执行结果
true

2.2 懒汉式

方式1(线程不安全)

/**
 * 懒汉式-线程不安全
 */
public class Singleton {
    //私有构造方法
    private Singleton() {}

    //在成员位置创建该类的对象
    private static Singleton instance;

    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

从上面代码可以看出该方式在成员位置声明Singleton类型的静态变量,并没有进行对象的赋值操作,那么什么时候赋值的呢?当调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了懒加载的效果。但是,如果是多线程环境,会出现线程安全问题。

单线程环境下

// 创建Singleton类的对象
Singleton instance01 = Singleton.getInstance();
Singleton instance02 = Singleton.getInstance();

System.out.println(instance01 == instance02);

// 执行结果
true

多线程环境下,创建了两个不同对象

public class Client02 {
    public static void main(String[] args) {
        Thread threadFoo = new Thread(new ThreadFoo());
        Thread threadBar = new Thread(new ThreadBar());
        threadFoo.start();
        threadBar.start();
    }

    static class ThreadFoo implements Runnable {
        @Override
        public void run() {
            Singleton singleton = Singleton.getInstance();
            System.out.println(singleton);
        }
    }

    static class ThreadBar implements Runnable {
        @Override
        public void run() {
            Singleton singleton = Singleton.getInstance();
            System.out.println(singleton);
        }
    }
}

//执行结果
com.patterns.singleton.demo03.Singleton@6857aa8a
com.patterns.singleton.demo03.Singleton@143da367

方式2(线程安全)

/**
 * 懒汉式-线程安全
 */
public class Singleton {
    //私有构造方法
    private Singleton() {}

    //在成员位置创建该类的对象
    private static Singleton instance;

    //对外提供静态方法获取该对象
    public static synchronized Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

该方式也实现了懒加载效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低。从上面代码可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。

public class Client {
    public static void main(String[] args) {
        Thread threadFoo = new Thread(new ThreadFoo());
        Thread threadBar = new Thread(new ThreadBar());
        threadFoo.start();
        threadBar.start();
    }

    static class ThreadFoo implements Runnable {
        @Override
        public void run() {
            Singleton singleton = Singleton.getInstance();
            System.out.println(singleton);
        }
    }

    static class ThreadBar implements Runnable {
        @Override
        public void run() {
            Singleton singleton = Singleton.getInstance();
            System.out.println(singleton);
        }
    }
}

//执行结果
com.patterns.singleton.demo04.Singleton@143da367
com.patterns.singleton.demo04.Singleton@143da367

方式3(双重检查锁)

方式二中提到synchronized加锁的问题,对于 getInstance() 方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以没必让每个线程必须持有锁才能调用该方法,所以需要调整加锁的时机,由此也产生了一种新的实现模式:双重检查锁模式

/**
 * 拥有双重校验锁机制的同步锁单例模式
 */
public class Singleton { 
    //私有构造方法
    private Singleton() {}

    private static Singleton instance;

   //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        //第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
        if(instance == null) {
            synchronized (Singleton.class) {
                //抢到锁之后再次判断是否为null
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。


要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile 关键字, volatile 关键字可以保证可见性和有序性。

/**
 * 双重检查方式
 */
public class Singleton {

    //私有构造方法
    private Singleton() {}

    private static volatile Singleton instance;

   //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        //第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
        if(instance == null) {
            synchronized (Singleton.class) {
                //抢到锁之后再次判断是否为空
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

添加 volatile 关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。

方式4(静态内部类方式)

静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。

/**
 * 静态内部类方式
 */
public class Singleton {

    //私有构造方法
    private Singleton() {}
    
    //定义静态内部类
    private static class SingletonHolder {
        //在内部类声明并初始化外部类的对象
        private static final Singleton INSTANCE = new Singleton();
    }

    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。

单线程环境下

// 创建Singleton类的对象
Singleton instance01 = Singleton.getInstance();
Singleton instance02 = Singleton.getInstance();

System.out.println(instance01 == instance02);

//执行结果
true

多线程环境下

public class Client {
    public static void main(String[] args) {
        Thread threadFoo = new Thread(new ThreadFoo());
        Thread threadBar = new Thread(new ThreadBar());
        threadFoo.start();
        threadBar.start();
    }

    static class ThreadFoo implements Runnable {
        @Override
        public void run() {
            Singleton singleton = Singleton.getInstance();
            System.out.println(singleton);
        }
    }

    static class ThreadBar implements Runnable {
        @Override
        public void run() {
            Singleton singleton = Singleton.getInstance();
            System.out.println(singleton);
        }
    }
}

//执行结果
com.patterns.singleton.demo07.Singleton@86953ce
com.patterns.singleton.demo07.Singleton@86953ce

静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。

2.3 枚举方式

枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。

/**
 * 枚举方式
 */
public enum Singleton {
    INSTANCE;
}

3. 存在的问题

破坏单例模式

使上面定义的单例类(Singleton)可以创建多个对象,枚举方式除外。有两种方式,分别是序列化和反射。

3.1 序列化反序列化破坏单例模式

破坏测试

Singleton类(使用静态内部类方式)

public class Singleton implements Serializable {

    //私有构造方法
    private Singleton() {}

    //定义静态内部类
    private static class SingletonHolder {
        //在内部类声明并初始化外部类的对象
        private static final Singleton INSTANCE = new Singleton();
    }

    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

Test类

public class Test{
    public static void main(String[] args) throws Exception {
        //往文件中写对象
        writeObjectFile();
        //从文件中读取对象
        Singleton s1 = readObjectFromFile();
        Singleton s2 = readObjectFromFile();

        System.out.println(s1);
        System.out.println(s2);
        //判断两个反序列化后的对象是否是同一个对象
        System.out.printf("s1=s2?: %s",s1 == s2);
    }

    private static Singleton readObjectFromFile() throws Exception {
        //创建对象输入流对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("src/com/patterns/singleton/demo09/test.txt"));
        //第一个读取Singleton对象
        Singleton instance = (Singleton) ois.readObject();
        //释放资源
        ois.close();

        return instance;
    }

    public static void writeObjectFile() throws Exception {
        //获取Singleton类的对象
        Singleton instance = Singleton.getInstance();
        //创建对象输出流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("src/com/patterns/singleton/demo09/test.txt"));
        //将instance对象写出到文件中
        oos.writeObject(instance);
        // 释放资源
        oos.close();
    }
}

执行结果

表明序列化和反序列化已经破坏了单例设计模式。

解决

在Singleton类中添加readResolve()方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象。

Singleton类

public class Singleton implements Serializable {

    //私有构造方法
    private Singleton() {}

    //定义静态内部类
    private static class SingletonHolder {
        //在内部类声明并初始化外部类的对象
        private static final Singleton INSTANCE = new Singleton();
    }

    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }

    /**
     * 下面是为了解决序列化反序列化破解单例模式
     */
    private Object readResolve() {
        return SingletonHolder.INSTANCE;
    }
}

从结果可见,确实解决了问题

3.2 反射破坏单例模式

破坏测试

Singleton类

public class Singleton {

    //私有构造方法
    private Singleton() {}

    //定义静态内部类
    private static class SingletonHolder {
        //在内部类声明并初始化外部类的对象
        private static final Singleton INSTANCE = new Singleton();
    }

    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

Test类

public class Test {
    public static void main(String[] args) throws Exception {
        // 获取Singleton的字节码对象
        Class<Singleton> singletonClass = Singleton.class;
        // 获取无参构造方法对象
        Constructor<Singleton> declaredConstructor = singletonClass.getDeclaredConstructor();
        // 取消访问检查
        declaredConstructor.setAccessible(true);
        // 创建Singleton对象
        Singleton singleton01 = declaredConstructor.newInstance();
        Singleton singleton02 = declaredConstructor.newInstance();
        // 判断通过反射创建的两个Singleton对象是否是同一个对象
        System.out.println(singleton01 == singleton02);
    }
}

执行结果

解决

当通过反射方式调用构造方法进行创建创建时,直接抛异常。不运行此中操作。

public class Singleton {

    private static boolean falg = false;

    //私有构造方法
    private Singleton() {
        synchronized (Singleton.class){
            // 判断flag的值是否是true,如果是true,说明不是第一次访问,抛一个异常,反之说明是第一次访问
            if(falg) {
                throw new RuntimeException("不能创建多个对象");
            }
            // 将flag的值设置为true
            falg = true;
        }
    }

    //定义静态内部类
    private static class SingletonHolder {
        //在内部类声明并初始化外部类的对象
        private static final Singleton INSTANCE = new Singleton();
    }

    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

通过debug发现是第二次创建对象抛出异常

4. Java 核心程序单例示例

以Runtime类为例,是饿汉式(静态属性)方式来实现单例模式的

使用Runtime类中的方法

public class RuntimeDemo {
    public static void main(String[] args) throws IOException {
        //获取Runtime类对象
        Runtime runtime = Runtime.getRuntime();

        //返回 Java 虚拟机中的内存总量
        System.out.println("Java虚拟机中的内存总量:"+runtime.totalMemory());

        //返回 Java 虚拟机试图使用的最大内存量
        System.out.println("Java虚拟机试图使用的最大内存量:"+runtime.maxMemory());

        //创建一个新的进程执行指定的字符串命令,返回进程对象
        Process process = runtime.exec("ipconfig");
        //获取命令执行后的结果,通过输入流获取
        InputStream inputStream = process.getInputStream();
        byte[] arr = new byte[1024 * 1024* 100];
        int b = inputStream.read(arr);
        System.out.println(new String(arr,0,b,"gbk"));
    }
}

执行结果

Java虚拟机中的内存总量:255328256
Java虚拟机试图使用的最大内存量:3787980800

Windows IP 配置


以太网适配器 Panda:

   媒体状态  . . . . . . . . . . . . : 媒体已断开连接
   连接特定的 DNS 后缀 . . . . . . . : 

以太网适配器 以太网:

   媒体状态  . . . . . . . . . . . . : 媒体已断开连接
   连接特定的 DNS 后缀 . . . . . . . : 

以太网适配器 VirtualBox Host-Only Network:

   连接特定的 DNS 后缀 . . . . . . . : 
   本地链接 IPv6 地址. . . . . . . . : fe80::f93d:49a6:d6fd:22d3%12
   IPv4 地址 . . . . . . . . . . . . : 192.168.56.1
   子网掩码  . . . . . . . . . . . . : 255.255.255.0
   默认网关. . . . . . . . . . . . . : 

无线局域网适配器 本地连接* 1:

   媒体状态  . . . . . . . . . . . . : 媒体已断开连接
   连接特定的 DNS 后缀 . . . . . . . : 

无线局域网适配器 本地连接* 2:

   媒体状态  . . . . . . . . . . . . : 媒体已断开连接
   连接特定的 DNS 后缀 . . . . . . . : 

以太网适配器 VMware Network Adapter VMnet1:

   连接特定的 DNS 后缀 . . . . . . . : 
   本地链接 IPv6 地址. . . . . . . . : fe80::91c2:2e78:567b:1d6%9
   IPv4 地址 . . . . . . . . . . . . : 192.168.184.1
   子网掩码  . . . . . . . . . . . . : 255.255.255.0
   默认网关. . . . . . . . . . . . . : 

以太网适配器 VMware Network Adapter VMnet8:

   连接特定的 DNS 后缀 . . . . . . . : 
   本地链接 IPv6 地址. . . . . . . . : fe80::386f:4c1a:86f1:edba%11
   IPv4 地址 . . . . . . . . . . . . : 192.168.42.1
   子网掩码  . . . . . . . . . . . . : 255.255.255.0
   默认网关. . . . . . . . . . . . . : 

无线局域网适配器 WLAN:

   连接特定的 DNS 后缀 . . . . . . . : 
   本地链接 IPv6 地址. . . . . . . . : fe80::582e:da14:67b8:d3fe%7
   IPv4 地址 . . . . . . . . . . . . : 192.168.1.110
   子网掩码  . . . . . . . . . . . . : 255.255.255.0
   默认网关. . . . . . . . . . . . . : 192.168.1.1