Skip to content
On this page

单例模式

单例模式(singleton Pattern)是java中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式。可以直接访问,不需要实例化该类的对象

单例模式的结构

单例模式主要有一下角色:

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

单例模式的实现

单例设计模式分类有两种:

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

饿汉式-方式1(静态变量方式)

java
package com.huangjiliang.design.heima.single;

/**
 * 单例模式-饿汉式-(静态变量方式)
 */
public class Singleton1 {

    private Singleton1() {}

    private static Singleton1 instance = new Singleton1();

    public static Singleton1 getInstance() {
        return instance;
    }

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

饿汉式-方式2(静态代码块方式)

java
package com.huangjiliang.design.heima.single;

/**
 * 单例模式-饿汉式(静态代码块方式)
 */
public class Singleton2 {
    
    private Singleton2() {}
    
    private static Singleton2 instance;
    
    static {
        instance = new Singleton2();
    }
    
    private Singleton2 getInstance() {
        return instance;
    }

}
  • 该方式再成员位置生命Singleton2类型的静态变量,而对象的创建时再静态代码块中,也是对着类的加载而创建。所以和饿汉式的静态变量方式基本上一样,即也会存在内存浪费的问题

懒汉式-方式1(线程不安全)

java
package com.huangjiliang.design.heima.single;

/**
 * 单例模式-懒汉式(线程不安全)
 */
public class Singleton3 {
    
    private Singleton3(){}
    
    private static Singleton3 instance;
    
    public static Singleton3 getInstance() {
        if (instance == null) {
            instance = new Singleton3();
        }
        return instance;
    }
}
  • 从上面代码我们可以看出该方式再成员位置声明Singleton3类型的静态变量,并没有进行对象的复制操作,那么什么时候赋值的呢?当调用getInstance()方法获取Singleton3类的对象的时候才会创建Singleton3类的对象,这样就实现了懒加载的效果。但是,如果是在多线程环境会出现线程安全问题

懒汉式-方式2(线程安全)

java
package com.huangjiliang.design.heima.single;

/**
 * 单例模式-懒汉式(线程安全)
 */
public class Singleton4 {

    private Singleton4(){}

    private static Singleton4 instance;

    /**
     * 加所
     * @return
     */
    public static synchronized Singleton4 getInstance() {
        if (instance == null) {
            instance = new Singleton4();
        }
        return instance;
    }
}
  • 该方式也实现了懒加载的效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低。从上面代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了

懒汉式-方式3(双重检查锁)

针对懒汉式-方式2(线程安全)加锁导致执行效果效率低问题,因为对于getInstamce()方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必要让每个线程都必须持有锁才能调用该方法,我们需要调整枷锁的时机,来提高获取对象的效率。由此也产生了一种新的实现模式:双重检查锁模式

java
package com.huangjiliang.design.heima.single;

/**
 * 单例模式-懒汉式(线程安全-双剑检查机制-提高效率)
 */
public class Singleton5 {

    private Singleton5(){}

    private static Singleton5 instance;

    /**
     * 加所
     * @return
     */
    public static Singleton5 getInstance() {
        // 第一次判断instance是否为null,不进行加锁操作,直接返回实例
        if (instance == null) {
            synchronized (Singleton5.class) {
                // 抢到锁之后再次判断是否为null
                if (instance == null) {
                    instance = new Singleton5();
                }
            }
        }
        return instance;
    }
}
  • 双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去是完美无缺的,其实是存在问题,在多线程情况下,可能会出现空指针问题。出现问题的原因是JVM在实例化对象的时候进行优化和指令重排序操作
  • 要解决双重检查锁模式带来空指针异常的问题,只需要使用volatile关键字,volatile关键字可以保证可见性和有序性
java
package com.huangjiliang.design.heima.single;

/**
 * 单例模式-懒汉式(线程安全-双剑检查机制-提高效率-保证可见性和有序性)
 */
public class Singleton6 {

    private Singleton6(){}

    // 增加volatile保证可见性和有序性
    private static volatile Singleton6 instance;

    /**
     * 加所
     * @return
     */
    public static Singleton6 getInstance() {
        // 第一次判断instance是否为null,不进行加锁操作,直接返回实例
        if (instance == null) {
            synchronized (Singleton6.class) {
                // 抢到锁之后再次判断是否为null
                if (instance == null) {
                    instance = new Singleton6();
                }
            }
        }
        return instance;
    }
}
  • 添加volatile关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证多线程的情况下线程安全也不会有性能问题

懒汉式-方式4(静态内部类方式)

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

java
package com.huangjiliang.design.heima.single;

/**
 * 单例模式-静态内部类方式
 */
public class Singleton7 {

    private Singleton7() {}

    private static class SingletonHolder {
        private static final Singleton7 INSTANCE = new Singleton7();
    }

    // 对外提供静态方法获取该对象实例
    public static Singleton7 getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
  • 第一次加载Singleton7类时是不会去初始化INSTANCE,只有第一次调用getInstance()方法,虚拟机才会加载内部类SingletonHolder,并初始化INSTANCE,这样不仅确保线程安全,也能保证Singleton7类的唯一性
  • 静态内部类单例模式时一种优秀的单例模式,时开源项目中比较常用的单例模式,在没有任何加锁的情况下,保证了多线程的安全,并且没有任何性能影响和空间的浪费

枚举方式

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

java
package com.huangjiliang.design.heima.single;

/**
 * 单例模式 - 枚举类方式
 */
public enum Singleton {
    
    INSTANCE;
    
}
  • 说明:枚举方式属于饿汉方式

存在问题

破坏单例模式

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

问题复现

序列化和反序列化

java
package com.huangjiliang.design.heima.single;

import java.io.Serializable;

/**
 * 单例模式-静态内部类
 */
public class Demo1 implements Serializable {

    private Demo1(){}

    public static Demo1 getInstance() {
        return SingleHolder.instance;
    }

    private static class SingleHolder {
        private static Demo1 instance = new Demo1();
    }

}
java
package com.huangjiliang.design.heima.single;

import java.io.*;

/**
 * 演示序列化和反序列化破坏Demo1类单例模式(静态内部类方式)
 */
public class Demo1Test {

    private static String filePath = "D:\\workSpace\\vitepress-notes\\docs\\other\\design\\create\\test.txt";

    public static void main(String[] args) throws Exception {
//        writeObject2File();
        Demo1 demo1 = readObjectFromFile();
        Demo1 demo11 = readObjectFromFile();
        // 两次获取实例对象,比对,发现输出false,证明两个实例对象不同,违反了单例模式
        System.out.println(demo11 == demo1);
    }

    public static Demo1 readObjectFromFile() throws IOException, ClassNotFoundException {
        // 创建对象输入流对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath));
        // 读取出对象
        Demo1 instance = (Demo1)ois.readObject();

        return instance;
    }

    /**
     * 将对象写到文件中
     * @throws IOException
     */
    public static void writeObject2File() throws IOException {
        // 获取Demo1类的对象
        Demo1 instance = Demo1.getInstance();
        // 创建对象输出流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath));
        // 将对象写到文件中
        oos.writeObject(instance);
    }

}
  • 两次从文件中获取实例对象,比对,发现输出false,证明两个实例对象不同,违反了单例模式

反射

java
package com.huangjiliang.design.heima.single;

/**
 * 创建型模式之单例模式
 * 双重检查
 */
public class Demo2 {

    private Demo2() {}

    private static volatile Demo2 instance;

    public static Demo2 getInstance() {
        if (instance != null) {
            return instance;
        }
        synchronized (Demo2.class) {
            if (instance != null) {
                return instance;
            }
            instance = new Demo2();
            return instance;
        }
    }
}
java
package com.huangjiliang.design.heima.single;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * 使用反射机制演示,破坏Demo2单例模式
 */
public class Demo2Test {

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        // 获取Demo2类的字节码对象
        Class<Demo2> demo2Class = Demo2.class;
        // 获取Demo2类的私有无参构造方法对象
        Constructor<Demo2> declaredConstructor = demo2Class.getDeclaredConstructor();
        // 取消访问检查
        declaredConstructor.setAccessible(true);
        // 创建对象
        Demo2 demo2 = declaredConstructor.newInstance();
        // 创建对象
        Demo2 demo21 = declaredConstructor.newInstance();
        // 判断通过反射创建的两个Demo2对象是否同一个对象。输出false
        System.out.println(demo21 == demo2);

    }
}
  • 上面代码运行结果是false,表示可以使用反射机制可以破坏单例模式

问题解决方案

序列化、反序列化破坏单例模式的解决方法

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

java
package com.huangjiliang.design.heima.single;

import java.io.Serializable;

/**
 * 单例模式-静态内部类
 */
public class Demo1 implements Serializable {

    private Demo1(){}

    public static Demo1 getInstance() {
        return SingletonHolder.instance;
    }

    private static class SingletonHolder {
        private static Demo1 instance = new Demo1();
    }

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

原理分析《ObjectInputstream类》

java
public final Object readObject() throws IOException, ClassNotFoundException{
    ...
    // if nested read, passHandle contains handle of enclosing object
    int outerHandle = passHandle;
    try {
        Object obj = readObject0(false);//重点查看readObject0方法
    .....
}
    
private Object readObject0(boolean unshared) throws IOException {
    ...
    try {
        switch (tc) {
            ...
            case TC_OBJECT:
                return checkResolve(readOrdinaryObject(unshared));//重点查看readOrdinaryObject方法
            ...
        }
    } finally {
        depth--;
        bin.setBlockDataMode(oldMode);
    }    
}
    
private Object readOrdinaryObject(boolean unshared) throws IOException {
    ...
    //isInstantiable 返回true,执行 desc.newInstance(),通过反射创建新的单例类,
    obj = desc.isInstantiable() ? desc.newInstance() : null; 
    ...
    // 在Singleton类中添加 readResolve 方法后 desc.hasReadResolveMethod() 方法执行结果为true
    if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {
        // 通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变量
        // 这样多次调用ObjectInputStream类中的readObject方法,继而就会调用我们定义的readResolve方法,所以返回的是同一个对象。
        Object rep = desc.invokeReadResolve(obj);
        ...
    }
    return obj;
}

反射方式破解单例的解决方法

java
package com.huangjiliang.design.heima.single;

/**
 * 创建型模式之单例模式
 * 双重检查
 */
public class Demo2 {

    private Demo2() {
        /**
         * 反射破解单例模式需要添加的代码
         */
        if(instance != null) {
            throw new RuntimeException();
        }
    }

    private static volatile Demo2 instance;

    public static Demo2 getInstance() {
        if (instance != null) {
            return instance;
        }
        synchronized (Demo2.class) {
            if (instance != null) {
                return instance;
            }
            instance = new Demo2();
            return instance;
        }
    }
}
  • 这种方法比较好理解。当通过反射方式调用构造函数方法进行创建时,直接抛出异常,不运行此中操作

JDK单例解析-Runtime类

Runtime类就是使用的单例设计模式

1、通过源代码查看使用的时哪几种单例模式

java
public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
    ...
}
  • 从上面源代码中可以看出Runtime类使用的是恶汉式(静态属性)方式来实现单例模式的。

2、使用Runtime类中的方法

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

        //返回 Java 虚拟机中的内存总量。
        System.out.println(runtime.totalMemory());
        //返回 Java 虚拟机试图使用的最大内存量。
        System.out.println(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"));
    }
}