单例设计模式

概述

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

单例设计模式分类两种:
饿汉式 :类加载就会导致该单实例对象被创建 。
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建。

饿汉式

静态变量方式

java 复制代码
public class Singleton {

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

    // 2.创建本类对象
    private static Singleton instance = new Singleton();

    // 3.提供公共的访问方式,让外界获取该对象
    public static Singleton getInstance() {
        return instance;
    }
}

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

java 复制代码
public class Client {
    public static void main(String[] args) {
        //创建Singleton类的对象
        Singleton instance = Singleton.getInstance();
        Singleton instance1 = Singleton.getInstance();
        //判断获取的对象是否是同一个
        System.out.println(instance == instance1);
    }
}

静态代码块方式

java 复制代码
public class Singleton {

    // 1. 构造器私有化
    private Singleton() {
    }

    // 2. 本类内部创建对象实例
    private static Singleton instance;//null

    // 3.在静态代码块中复制
    static {
        instance = new Singleton();
    }

    // 4. 提供一个公有的静态方法,返回实例对象
    public static Singleton getInstance() {
        return instance;
    }
}

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

懒汉式

线程不安全

java 复制代码
public class Singleton {

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

    //声明Singleton类型的变量instance
    private static Singleton instance;//只是声明一个该类型的变量,并没有进行赋值

    //对外提供访问方式
    public static Singleton getInstance() {
        //判断instance是否为null,如果为null,则创建对象并赋值给instance,
        // 如果不为null,则直接返回instance
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

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

线程安全

java 复制代码
public class Singleton {

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

    //声明Singleton类型的变量instance
    private static Singleton instance;//只是声明一个该类型的变量,并没有进行赋值

    //对外提供访问方式
    public static synchronized Singleton getInstance() {
        //判断instance是否为null,如果为null,则创建对象并赋值给instance,
        // 如果不为null,则直接返回instance
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

双重检查锁

java 复制代码
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 关键字可以保证可见性和有序性。

java 复制代码
public class Singleton { //私有构造方法
    private Singleton() {
    }

    private static volatile Singleton instance; //对外提供静态方法获取该对象

    public static Singleton getInstance() {
        //第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
        if (instance == null) {
            synchronized (Singleton.class) {
                //抢到锁之后再次判断是否为null
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

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

静态内部类方式

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

java 复制代码
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 类的唯一性。

静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任

何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。

枚举方式

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

java 复制代码
public enum Singleton {
    INSTANCE;
}

存在的问题

破坏单例模式:

  • 序列化反序列化
java 复制代码
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;
    }
}
java 复制代码
public class Client {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
//        writeDataToFile();
        readDataFromFile();
        readDataFromFile();
    }

    //从文件读取数据(对象)
    public static void readDataFromFile() throws IOException, ClassNotFoundException {
        //1.创建对象输入流对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\javacode\\design-patterns\\src\\main\\resources\\a.txt"));
        //2.读取对象
        Singleton instance = (Singleton) ois.readObject();

        System.out.println(instance);
        //3.释放资源
        ois.close();
    }

    //向文件中写数据(对象)
    public static void writeDataToFile() throws IOException {
        // 1.获取Singleton类的对象
        Singleton instance = Singleton.getInstance();
        //2.创建对象输出流对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\javacode\\design-patterns\\src\\main\\resources\\a.txt"));
        //3.写对象
        oos.writeObject(instance);
        //4.释放资源
        oos.close();
    }
}

上面代码运行结果是 false ,表明序列化和反序列化已经破坏了单例设计模式。

解决方案

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

java 复制代码
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;
    }
}
  • 反射
java 复制代码
public class Singleton implements Serializable {

    private static boolean flag = false;

    //私有构造方法
    private Singleton() {
        synchronized (Singleton.class){
            if(flag){
                throw new RuntimeException("不能创建多个对象");
            }
            flag = true;
        }
    }

    //定义一个静态内部类
    private static class SingletonHolder {
        //在内部类中声明并初始化外部类的对象
        private static final Singleton INSTANCE = new Singleton();
    }
}
java 复制代码
public class Client {
    public static void main(String[] args) throws Exception {
        //获取Singleton类的字节码对象
        Class clazz = Singleton.class;
        //获取Singleton类的私有无参构造方法对象
        Constructor constructor = clazz.getDeclaredConstructor();
        //取消访问检查
        constructor.setAccessible(true);
        //创建Singleton类的对象s1
        Singleton s1 = (Singleton) constructor.newInstance();
        //创建Singleton类的对象s2
        Singleton s2 = (Singleton) constructor.newInstance();
        //判断通过反射创建的两个Singleton对象是否是同一个对象
        System.out.println(s1 == s2);
    }
}

上面代码运行结果是 false ,表明序列化和反序列化已经破坏了单例设计模式
注意:枚举方式不会出现这两个问题。

解决方案

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

java 复制代码
public class Singleton implements Serializable {

    private static boolean flag = false;

    //私有构造方法
    private Singleton() {
        synchronized (Singleton.class){
            if(flag){
                throw new RuntimeException("不能创建多个对象");
            }
            flag = true;
        }
    }

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

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

举例

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

java 复制代码
package pattern.singleton.demo9;

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类使用的是恶汉式(静态属性)方式来实现单例模式的。

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"));
    }
}
相关推荐
AI的魔盒2 分钟前
基于Java与MAVLink协议的多无人机(Cube飞控)集群控制与调度方案问题
java·开发语言·无人机
北执南念40 分钟前
项目代码生成工具
java
中国lanwp1 小时前
springboot logback 默认加载配置文件顺序
java·spring boot·logback
苹果酱05671 小时前
【Azure Redis 缓存】在Azure Redis中,如何限制只允许Azure App Service访问?
java·vue.js·spring boot·mysql·课程设计
胡子发芽2 小时前
请详细解释Java中的线程池(ThreadPoolExecutor)的工作原理,并说明如何自定义线程池的拒绝策略
java
沫夕残雪2 小时前
Tomcat的安装与配置
java·tomcat
胡子发芽2 小时前
请解释Java中的NIO(New I/O)与传统I/O的区别,并说明NIO中的关键组件及其作用
java
柚个朵朵2 小时前
IDEA中使用Git
java·git·spring
jerry6093 小时前
优先队列、堆笔记(算法第四版)
java·笔记·算法