设计模式学习04(Java实现)——单例模式

写在前面

  • 记录学习设计模式的笔记
  • 提高对设计模式的灵活运用

学习地址

https://www.bilibili.com/video/BV1G4411c7N4

https://www.bilibili.com/video/BV1Np4y1z7BU

参考文章

http://c.biancheng.net/view/1317.html

项目源码
https://gitee.com/zhuang-kang/DesignPattern

4,创建者模式

创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”。

这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。

创建型模式分为:

  • 单例模式
  • 工厂方法模式
  • 抽象工程模式
  • 原型模式
  • 建造者模式

5,单例模式

5.1 单例模式的定义和特点

单例(Singleton)模式的定义: 指一个类只有一个实例,且该类能自行创建这个实例的一种模式。例如,Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。

在计算机系统中,还有 Windows 的回收站、操作系统中的文件系统、多线程中的线程池、显卡的驱动程序对象、打印机的后台处理服务、应用程序的日志对象、数据库的连接池、网站的计数器、Web 应用的配置对象、应用程序中的对话框、系统中的缓存等常常被设计成单例。

单例模式在现实生活中的应用也非常广泛,例如公司 CEO、部门经理等都属于单例模型。J2EE 标准中的 ServletgContext 和 ServletContextConfig、Spring 框架应用中的 ApplicationContext、数据库中的连接池等也都是单例模式。

单例模式有 3 个特点:

  1. 单例类只有一个实例对象;
  2. 该单例对象必须由单例类自行创建;
  3. 单例类对外提供一个访问该单例的全局访问点。

单例模式的优点

  • 单例模式可以保证内存里只有一个实例,减少了内存的开销。
  • 可以避免对资源的多重占用。
  • 单例模式设置全局访问点,可以优化和共享资源的访问。

单例模式的缺点:

  • 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
  • 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
  • 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。

5.2 单例模式的结构与实现

5.2.1 单例模式的结构

  1. 单例类:包含一个实例且能自行创建这个实例的类。
  2. 访问类:使用单例的类。

5.2 代码实现

单例设计模式分类两种:

饿汉式:类加载就会导致该单实例对象被创建

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

饿汉式(静态变量)

package com.zhuang.singleton.type1;

/**
 * @Classname SingletonTest01
 * @Description  饿汉式(静态变量)
 * @Date 2021/3/17 9:26
 * @Created by dell
 */

public class SingletonTest01 {
    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        //判断是否为单例
        System.out.println(instance == instance2);
        System.out.println("intstance的哈希值" + instance.hashCode());
        System.out.println("intstance2的哈希值" + instance2.hashCode());
    }
}

class Singleton {

    //1.构造器私有化,外部能new、
    private Singleton() {

    }

    //本类内部创建对象实例
    private final static Singleton instance = new Singleton();

    //对外部提供一个公有的静态方法
    public static Singleton getInstance() {
        return instance;
    }
}

说明:

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

静态代码块

package com.zhuang.singleton.type2;

/**
 * @Classname SingletonTest02
 * @Description  静态代码块
 * @Date 2021/3/17 9:35
 * @Created by dell
 */

public class SingletonTest02 {
    public static void main(String[] args) {
        Singleton2 instance = Singleton2.getInstance();
        Singleton2 instance2 = Singleton2.getInstance();
        //判断是否为单例
        System.out.println(instance == instance2);
        System.out.println("intstance的哈希值" + instance.hashCode());
        System.out.println("intstance2的哈希值" + instance2.hashCode());
    }
}

class Singleton2 {

    //1.构造器私有化,外部能new、
    private Singleton2() {

    }

    //本类内部创建对象实例
    private static Singleton2 instance;

    /*
    在静态代码块中创建对象
     */
    static {
        instance = new Singleton2();
    }

    //对外部提供一个公有的静态方法
    public static Singleton2 getInstance() {
        return instance;
    }
}

说明:

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

懒汉式 线程不安全

package com.zhuang.singleton.type3;

/**
 * @Classname SingletonTest03
 * @Description  懒汉式 线程不安全
 * @Date 2021/3/17 9:39
 * @Created by dell
 */

public class SingletonTest03 {
    public static void main(String[] args) {
        System.out.println("懒汉式,线程不安全!!!");
        Singleton instance = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        //判断是否为单例
        System.out.println(instance == instance2);
        System.out.println("intstance的哈希值" + instance.hashCode());
        System.out.println("intstance2的哈希值" + instance2.hashCode());
    }
}

class Singleton {
    private static Singleton instance;

    private Singleton() {

    }

    //提供一个静态的公有方法 当使用到该方法时,才去创建instance
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

说明:

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

懒汉式(线程安全 , 同步方法)

package com.zhuang.singleton.type4;

/**
 * @Classname SingletonTest04
 * @Description  懒汉式(线程安全 , 同步方法)
 * @Date 2021/3/17 9:46
 * @Created by dell
 */

public class SingletonTest04 {
    public static void main(String[] args) {
        System.out.println("懒汉式,线程安全!!!");
        Singleton instance = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        //判断是否为单例
        System.out.println(instance == instance2);
        System.out.println("intstance的哈希值" + instance.hashCode());
        System.out.println("intstance2的哈希值" + instance2.hashCode());
    }
}

class Singleton {
    private static Singleton instance;

    private Singleton() {

    }

    //提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

说明:

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

懒汉式(线程安全 , 同步代码块)

package com.zhuang.singleton.type5;

/**
 * @Classname SingletonTest05
 * @Description  懒汉式(线程安全 , 同步代码块)
 * @Date 2021/3/17 9:50
 * @Created by dell
 */

public class SingletonTest05 {
    public static void main(String[] args) {
        System.out.println("懒汉式,线程安全!,同步代码块");
        Singleton instance = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        //判断是否为单例
        System.out.println(instance == instance2);
        System.out.println("intstance的哈希值" + instance.hashCode());
        System.out.println("intstance2的哈希值" + instance2.hashCode());
    }
}

class Singleton {
    private static Singleton instance;

    private Singleton() {

    }

    //提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                instance = new Singleton();
            }
        }
        return instance;
    }
}

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

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

package com.zhuang.singleton.type6;

/**
 * @Classname SingletonTest06
 * @Description  双重检查,推荐使用
 * @Date 2021/3/17 9:54
 * @Created by dell
 */

public class SingletonTest06 {
    public static void main(String[] args) {
        System.out.println("懒汉式,双重检查,推荐使用");
        Singleton instance = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        //判断是否为单例
        System.out.println(instance == instance2);
        System.out.println("intstance的哈希值" + instance.hashCode());
        System.out.println("intstance2的哈希值" + instance2.hashCode());
    }
}

class Singleton {
    private static volatile Singleton instance;

    private Singleton() {

    }

    //提供一个静态的公有方法,加入双重检查代码,加入同步处理的代码,解决懒加载的问题
    //保证效率。推荐使用
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                instance = new Singleton();
            }
        }
        return instance;
    }
}

小结:

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

静态内部类实现单例模式!

package com.zhuang.singleton.type7;

/**
 * @Classname SingletonTest07
 * @Description  静态内部类实现单例模式!
 * @Date 2021/3/17 9:59
 * @Created by dell
 */

public class SingletonTest07 {
    public static void main(String[] args) {
        System.out.println("静态内部类实现单例模式");
        Singleton instance = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        //判断是否为单例
        System.out.println(instance == instance2);
        System.out.println("intstance的哈希值" + instance.hashCode());
        System.out.println("intstance2的哈希值" + instance2.hashCode());
    }
}

class Singleton {
    private static Singleton instance;

    private Singleton() {

    }

    //写一个静态内部类,该类中有个静态属性,Singleton
    public static class SingletonInstance {
        private static final Singleton INSTANCE = new Singleton();
    }

    //提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE;
    public static synchronized Singleton getInstance() {
        return SingletonInstance.INSTANCE;
    }
}

说明:

第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder

并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。

小结:

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

枚举的方式实现单例模式

package com.zhuang.singleton.type8;

/**
 * @Classname SingletonTest08
 * @Description  枚举的方式实现单例模式
 * @Date 2021/3/17 10:06
 * @Created by dell
 */

public class SingletonTest08 {
    public static void main(String[] args) {
        System.out.println("枚举的方式实现单例模式,推荐使用");
        Singleton instance = Singleton.INSTANCE;
        Singleton instance2 = Singleton.INSTANCE;
        System.out.println(instance == instance2);
        //判断是否为单例
        System.out.println(instance == instance2);
        System.out.println("intstance的哈希值" + instance.hashCode());
        System.out.println("intstance2的哈希值" + instance2.hashCode());
    }
}

/*
枚举
 */
enum Singleton {
    INSTANCE;//属性

    public void method() {
        System.out.println("method()方法被调用...");
    }
}

说明:

枚举方式属于恶汉式方式。

5.3 单例模式的应用场景

  • 需要频繁创建的一些类,使用单例可以降低系统的内存压力,减少 GC。
  • 某类只要求生成一个对象的时候,如一个班中的班长、每个人的身份证号等。
  • 某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用。
  • 某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
  • 频繁访问数据库或文件的对象。
  • 对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,如果有多个实例,则系统会完全乱套。
  • 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等。

5.4 存在的问题

破坏单例模式:

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

  • 序列化反序列化

    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 {
            //往文件中写对象
            //writeObject2File();
            //从文件中读取对象
            Singleton s1 = readObjectFromFile();
            Singleton s2 = readObjectFromFile();
    
            //判断两个反序列化后的对象是否是同一个对象
            System.out.println(s1 == s2);
        }
    
        private static Singleton readObjectFromFile() throws Exception {
            //创建对象输入流对象
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\dell\\Desktop\\a.txt"));
            //第一个读取Singleton对象
            Singleton instance = (Singleton) ois.readObject();
    
            return instance;
        }
    
        public static void writeObject2File() throws Exception {
            //获取Singleton类的对象
            Singleton instance = Singleton.getInstance();
            //创建对象输出流
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\dell\\Desktop\\a.txt"));
            //将instance对象写出到文件中
            oos.writeObject(instance);
        }
    }
    

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

  • 反射

    Singleton类:

    public class Singleton {
    
        //私有构造方法
        private Singleton() {}
        
        private static volatile Singleton instance;
    
        //对外提供静态方法获取该对象
        public static Singleton getInstance() {
    
            if(instance != null) {
                return instance;
            }
    
            synchronized (Singleton.class) {
                if(instance != null) {
                    return instance;
                }
                instance = new Singleton();
                return instance;
            }
        }
    }
    

    Test类:

    public class Test {
        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,表明序列化和反序列化已经破坏了单例设计模式

注意:枚举方式不会出现这两个问题。

问题的解决

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

    在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;
        }
    }
    

    源码解析:

    ObjectInputStream类

    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;
    }
    
  • 反射方式破解单例的解决方法

    public class Singleton {
    
        //私有构造方法
        private Singleton() {
            /*
               反射破解单例模式需要添加的代码
            */
            if(instance != null) {
                throw new RuntimeException();
            }
        }
        
        private static volatile Singleton instance;
    
        //对外提供静态方法获取该对象
        public static Singleton getInstance() {
    
            if(instance != null) {
                return instance;
            }
    
            synchronized (Singleton.class) {
                if(instance != null) {
                    return instance;
                }
                instance = new Singleton();
                return instance;
            }
        }
    }
    

    说明:

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

5.5 JDK源码解析-Runtime类

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

  1. 通过源代码查看使用的是哪儿种单例模式

    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 Runtime are instance
         * methods and must be invoked with respect to the current runtime object.
         *
         * @return  the Runtime object associated with the current
         *          Java application.
         */
        public static Runtime getRuntime() {
            return currentRuntime;
        }
    
        /** Don't let anyone else instantiate this class */
        private Runtime() {}
        ...
    }
    

    从上面源代码中可以看出Runtime类使用的是恶汉式(静态属性)方式来实现单例模式的。

  1. 使用Runtime类中的方法

    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"));
        }
    }
    

写在最后

  • 如果我的文章对你有用,请给我点个,感谢你!
  • 有问题,欢迎在评论区指出!

你可能感兴趣的:(设计模式学习04(Java实现)——单例模式)