当前位置:

首页 > 编程开发 > Java设计模式的单例模式如何实现

Java设计模式的单例模式如何实现

单例模式单例模式顾名思义就是单一的实例,涉及到一个单一的类,该类负责创建自己的对象,同时确保只有一个对象被创建,并且提供一种可以访问这个对象的方式,可以直接访问,不需要实例化该类的对象。单例模式的特点:1.单例类只能有一个实例2.这个实例必须由单例类自己创建3.单例类需要提供给外界访问这个实例单例模式的作用:单例模式主要为了保证在Java应用程序中,一个类只有一个实例存在。1.单例模式的结构单例模式主要有以下角色:单例类只能创建一个实例的类访问类测试类,就是使用单例类的类2.单例模式的实现2.1饿汉式饿汉

本文目录

    单例模式

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

    单例模式的特点:

    1.单例类只能有一个实例

    2.这个实例必须由单例类自己创建

    3.单例类需要提供给外界访问这个实例

    单例模式的作用:

    单例模式主要为了保证在Java应用程序中,一个类只有一个实例存在。

    1.单例模式的结构

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

    • 单例类

    只能创建一个实例的类

    • 访问类

    测试类,就是使用单例类的类

    2.单例模式的实现

    2.1饿汉式

    饿汉式:类加载时创建该单实例类对象

    1.饿汉式-方式1 静态成员变量

    创建 饿汉式静态成员变量 单例类

    public class Demo1 {
     
        /**
         *私有构造方法  让外界不能创建该类对象
         */
        private Demo1(){}
    
        /**
         * 在类中创建该本类对象 static是由于外界获取该类对象的方法getInstance()是 static
         * 这个对象instance就是静态成员变量
         */
        private static Demo1 instance = new Demo1();
    
        /**
         * 提供一个公共的访问方式,让外界可以获取该类的对象 static是因为外界不需要创建对象,直接通过类访问
         */
        public static Demo1 getInstance(){
            return instance;
        }
    }

    创建 饿汉式静态成员变量 测试类(访问类)

    public class Test1 {
        public static void main(String[] args) {
          //创建demo1类的对象 这个时候就无法通过new创建了,因为demo1的构造方法是私有的
            Demo1 instance = Demo1.getInstance();
    
            Demo1 instance1 = Demo1.getInstance();
    
            //判断两个对象是否是同一个
            System.out.println(instance == instance1);
            
        }
    }

    输出true 表明是同一个对象,指向同一块内存地址,这样我们就保证了Demo1单例类只有一个对象被创建

    2.饿汉式-方式2 静态代码块

    创建 饿汉式静态代码块 单例类

    public class Demo2 {
        //饿汉式单例类  静态代码块
    
        /**
         *私有构造方法  让外界不能创建该类对象
         */
        private Demo2(){}
    
        /**
         *  声明一个静态的成员变量instance但是不赋值(不创建对象)
         *  没有为instance赋值,默认为null
         */
        private static  Demo2 instance;
    
        /**
         * 在静态代码快中为instance赋值(创建对象)
         */
        static {
            instance = new Demo2();
        }
        /**
         * 提供一个公共的访问方式,让外界可以获取该类的对象 static是因为外界不需要创建对象,直接通过类访问
         */
        public static Demo2  getInstance(){
            return instance;
        }
    }

    创建 饿汉式静态代码块 测试类

    public class Test2 {
        public static void main(String[] args) {
            Demo2 instance = Demo2.getInstance();
    
            Demo2 instance1 = Demo2.getInstance();
    
            System.out.println(instance == instance1);
        }
    }

    输出true 表明是同一个对象,指向同一块内存地址,这样我们就保证了Demo2单例类只有一个对象被创建

    3.饿汉式-方式3(枚举方式)

    枚举类实现单例模式是十分推荐的一种单例实现模式,由于枚举类型是线程安全的,并且只会加载一次,这是十分符合单例模式的特点的,枚举的写法很简单,而且枚举方式是所有单例实现中唯一一个不会被破环的单例实现模式

    单例类

    //枚举方式创建单例
    public enum Singleton {
         INSTANCE;
    }

    测试类

    public class Test1 {
        public static void main(String[] args) {
        Singleton instance = Singleton.INSTANCE;
        Singleton instance1 = Singleton.INSTANCE;
    
    
            System.out.println(instance == instance1);
            //输出 true
            
        }
    }

    注意:

    由于枚举方式是饿汉式,因此根据饿汉式的特点,枚举方式也会造成内存浪费,但是在不考虑内存问题下,枚举方式是首选,毕竟实现最简单了

    2.2懒汉式

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

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

    public class Demo3 {
        /**
         *私有构造方法  让外界不能创建该类对象
         */
        private Demo3(){}
    
        /**
         * 在类中创建该本类对象 static是由于外界获取该类对象的方法getInstance()是 static
         * 没有进行赋值(创建对象)
         */
        private static  Demo3 instance;
    
    
        /**
         * 提供一个公共的访问方式,让外界可以获取该类的对象 static是因为外界不需要创建对象,直接通过类访问
         */
        public static Demo3 getInstance(){
            //在首次使用该对象时创建,因此instance赋值也就是对象创建 就是在外界获取该单例类的方法getInstance()中创建
            instance = new Demo3();
            return instance;
        }
    
    }
    public class Test3 {
        public static void main(String[] args) {
            Demo3 instance = Demo3.getInstance();
    
            Demo3 instance1 = Demo3.getInstance();
            //判断两个对象是否是同一个
            System.out.println(instance == instance1);
        }
    }

    输出结果为false,表明我们创建懒汉式单例失败了。是因为我们在调用getInstance()时每次调用都会new一个实例对象,那么也就必然不可能相等了。

       // 如果instance为null,表明还没有创建该类的对象,那么就进行创建
            if(instance == null){
              instance = new Demo3();
            }
            //如果instance不为null,表明已经创建过该类的对象,根据单例类只能创建一个对象的特点,因此         //我们直接返回instance
            return instance;
        }

    注意:

    我们在测试是只是单线程,但是在实际应用中必须要考虑到多线程的问题。我们假设一种情况,线程1进入if判断然后还没来得及创建instance,这个时候线程1失去了cpu的执行权变为阻塞状态,线程2获取cpu执行权,然后进行if判断此时instance还是null,因此线程2为instance赋值创建了该单例对象,那么等到线程1再次获取cpu执行权,也进行了instance赋值创建了该单例对象,单例模式被破坏。

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

    我们可以通过加synchronized同步锁的方式保证单例模式在多线程下依旧有效

     public static synchronized Demo3 getInstance(){
            //在首次使用该对象时创建,因此instance赋值也就是对象创建 就是在外界获取该单例类的方法getInstance()中创建
    
    
            // 如果instance为null,表明还没有创建该类的对象,那么就进行创建
    
            if(instance == null){
              instance = new Demo3();
            }
            //如果instance不为null,表明已经创建过该类的对象,根据单例类只能创建一个对象的特点,因此我们直接返回instance
            return instance;
        }

    注意:

    虽然保证了线程安全问题,但是在getInstance()方法上添加了synchronized关键字,导致该方法执行效率很低(这是加锁的一个常见问题)。其实我们可以很容易发现,我们只是在判断instance时需要解决多线程的安全问题,而没必要在getInstance()上加锁

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

    对于getInstance()方法来说,绝大部分的操作都是读操作,读操作是线程安全的,没必要让每个线程必须持有锁才能调用该方法,我们可以调整加锁的时机。

    public class Demo4 {
        /**
         *私有构造方法  让外界不能创建该类对象
         */
        private Demo4(){}
    
        /**
         *
         * 没有进行赋值(创建对象) 只是声明了一个该类的变量
         */
        private static Demo4 instance;
    
    
        /**
         * 提供一个公共的访问方式,让外界可以获取该类的对象 static是因为外界不需要创建对象,直接通过类访问
         */
        public static  Demo4 getInstance(){
    
    
            // (第一次判断)如果instance为null,表明还没有创建该类的对象,那么就进行创建
            if(instance == null){
                synchronized (Demo4.class){
                    //第二次判断 如果instance不为null
                    if(instance == null){
                        instance = new Demo4();
                    }
                }
    
            }
    
            //如果instance不为null,表明已经创建过该单例类的对象,不需要抢占锁,直接返回
            return instance;
        }
    
    }

    双重检查锁模式完美的解决了单例、性能、线程安全问题,但是只是这样还是有问题的…

    JVM在创建对象时会进行优化和指令重排,在多线程下可能会发生空指针异常的问题,可以使用volatile关键字,volatile可以保证可见性和有序性。

     private static volatile Demo4  instance;

    Java设计模式的单例模式如何实现

    如果发生指令重排 2 和 3 的步骤颠倒,那么instance会指向一块虚无的内存(也有可能是有数据的一块内存)

    完整代码

    public class Demo4 {
        /**
         *私有构造方法  让外界不能创建该类对象
         */
        private Demo4(){}
    
        /**
         * volatile可以保证有序性
         * 没有进行赋值(创建对象) 只是声明了一个该类的变量
         */
        private static volatile Demo4  instance;
        
        /**
         * 提供一个公共的访问方式,让外界可以获取该类的对象 static是因为外界不需要创建对象,直接通过类访问
         */
        public static  Demo4 getInstance(){
            // (第一次判断)如果instance为null,表明还没有创建该类的对象,那么就进行创建
            if(instance == null){
                synchronized (Demo4.class){
                    //第二次判断 如果instance不为null
                    if(instance == null){
                        instance = new Demo4();
                    }
                }
            }
    
            //如果instance不为null,表明已经创建过该单例类的对象,不需要抢占锁,直接返回
            return instance;
        }
    }

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

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

    创建单例类

    public class Singleton {
    
        private Singleton(){}
    
        /**
         *定义一个静态内部类
         */
        private static  class SingletonHolder{
            //在静态内部类中创建外部类的对象
            private static final Singleton INSTANCE = new Singleton();
        }
    
        public static Singleton getInstance(){
            return SingletonHolder.INSTANCE;
        }
    }

    创建测试类

    public class Test4 {
        public static void main(String[] args) {
            Singleton instance = Singleton.getInstance();
    
            Singleton instance1 = Singleton.getInstance();
            //判断两个对象是否是同一个
            System.out.println(instance == instance1);
        }
    }

    注意:

    第一次加载Singleton类时不会去初始化INSTANCE,只有在调用getInstance()方法时,JVM加载SingletonHolder并初始化INSTANCE,这样可以保证线程安全,并且Singleton类的唯一性

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

    3.单例模式的破坏

    单例模式最重要的一个特点就是只能创建一个实例对象,那么如果能使单例类能创建多个就破坏了单例模式(除了枚举方式)破坏单例模式的方式有两种:

    3.1序列化和反序列化

    从以上创建单例模式的方式中任选一种(除枚举方式),例如静态内部类方式

    //记得要实现Serializable序列化接口
    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;
        }
    }

    测试类

    public class Test1 {
    
        public static void main(String[] args) throws IOException {
                  writeObjectToFile();
        }
    
    
        /**
         * 向文件中写数据(对象)
         * @throws IOException
         */
        public static void writeObjectToFile() throws IOException {
            //1.获取singleton对象
            Singleton instance = Singleton.getInstance();
            //2.创建对象输出流对象
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:\\1.txt"));
            //3.写对象
            oos.writeObject(instance);
            //4.释放资源
            oos.close();
    
    
        }
    }

    在d盘根目录下出现一个文件1.txt由于数据是序列化后的 咱也看不懂

    然后我们从这个文件中读取instance对象

    public static void main(String[] args) throws Exception {
                 // writeObjectToFile();
            readObjectFromFile();
            readObjectFromFile();
        }
        /**
         * 从文件中读数据(对象)
         * @throws Exception
         */
        public static  void readObjectFromFile() throws Exception {
    
            //1.创建对象输入流对象
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:\\1.txt"));
            //2.读对象
            Singleton instance = (Singleton) ois.readObject();
            System.out.println(instance);
            //3.释放资源
            ois.close();
        }

    输出结果不相同,结论为:序列化破坏了单例模式,两次读的对象不一样了

    com.xue.demo01.Singleton@2328c243
    com.xue.demo01.Singleton@bebdb06

    解决方案

    在singleton中添加readResolve方法

      /**
         * 当进行反序列化时,会自动调用该方法,将该方法的返回值直接返回
         * @return
         */
        public Object readResolve(){
            return SingletonHolder.INSTANCE;
        }

    重新进行写和读,发现两次读的结果是相同的,解决了序列化破坏单例模式的问题

    为什么在singleton单例类中添加readResolve方法就可以解决序列化破坏单例的问题呢,我们在ObjectInputStream源码中在readOrdinaryObject方法中

     private Object readOrdinaryObject(boolean unshared)
            throws IOException{
    //代码段   
    Object obj;
            try {
                //isInstantiable如果一个实现序列化的类在运行时被实例化就返回true
                //desc.newInstance()会通过反射调用无参构造创建一个新的对象
                obj = desc.isInstantiable() ? desc.newInstance() : null;
            } catch (Exception ex) {
                throw (IOException) new InvalidClassException(
                    desc.forClass().getName(),
                    "unable to create instance").initCause(ex);
            }
    
       //代码段
    
       if (obj != null &&
                handles.lookupException(passHandle) == null &&
           //hasReadResolveMethod 如果实现序列化接口的类中定义了readResolve方法就返回true
                desc.hasReadResolveMethod())
            {
           //通过反射的方式调用被反序列化类的readResolve方法
                Object rep = desc.invokeReadResolve(obj);
                if (unshared && rep.getClass().isArray()) {
                    rep = cloneArray(rep);
                }
           
        //代码段
     }
    3.2反射

    从以上创建单例模式的方式中任选一种(除枚举方式),例如静态内部类方式

    测试类

    public class Test1 {
    
        public static void main(String[] args) throws Exception {
    
    
            //1.获取Singleton的字节码对象
            Class singletonClass = Singleton.class;
    
            //2.获取无参构造方法对象
            Constructor cons = singletonClass.getDeclaredConstructor();
    
            //3.取消访问检查
            cons.setAccessible(true);
            //4.反射创建对象
            Singleton instance1 = (Singleton) cons.newInstance();
    
            Singleton instance2 = (Singleton) cons.newInstance();
    
            System.out.println(instance1 == instance2);
            //输出false 说明反射破坏了单例模式
        }
    
    
    }

    解决方案:

    public class Singleton  {
    
        //static是为了都能访问
        private static boolean flag = false;
    
        private Singleton() {
            //加上同步锁,防止多线程并发问题
            synchronized (Singleton.class) {
                //判断flag是否为true,如果为true说明不是第一次创建,抛异常
                if (flag) {
                    throw new RuntimeException("不能创建多个对象");
                }
                //flag的值置为true
                flag = true;
            }
        }
    
        /**
         *定义一个静态内部类
         */
        private static  class SingletonHolder{
            //在静态内部类中创建外部类的对象
            private static final Singleton INSTANCE = new Singleton();
        }
    
        public static Singleton getInstance(){
            return SingletonHolder.INSTANCE;
        }
    }
    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发 Java
    相关文章 更多
    Java单元测试入门:使用JUnit 5编写第一个测试用例
    Java单元测试入门:使用JUnit 5编写第一个测试用例

    面向初学者的JUnit 5实用入门指南。从测试概念、Maven依赖配置,到手把手编写首个单元测试用例及断言验证,助你掌握Java单元测试的核心流程。

    Java变量未初始化报错解析:局部变量与成员变量默认值差异
    Java变量未初始化报错解析:局部变量与成员变量默认值差异

    深入解析Java中局部变量未初始化编译报错与成员变量自动赋予默认值的机制差异,涵盖JVM内存分配、编译期确定性赋值检查与实际排查修复方案。

    Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat
    Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat

    详细讲解在Java中获取当前系统时间并进行格式化输出的两种核心方案:基于Java 8全新日期时间API的DateTimeFormatter与传统遗留的SimpleDateFormat,包含完整代码示例、线程安全机制对比及生产选型建议。

    Java中final与static final常量的定义及初始化规则
    Java中final与static final常量的定义及初始化规则

    深入解析Java中final变量与static final常量的定义、内存语义、初始化时机及合法赋值路径,涵盖编译期常量优化与实战避坑指南。

    Java抽象方法与接口方法的区别及选型建议
    Java抽象方法与接口方法的区别及选型建议

    深入对比Java中抽象类抽象方法与接口方法的语法演进、设计意图及运行期差异,并提供清晰的架构选型决策流程与实战范例。

    Java实现HTTP请求与JSON解析的完整代码示例
    Java实现HTTP请求与JSON解析的完整代码示例

    本文提供Java标准库HttpClient发起HTTP GET/POST请求并结合Jackson进行JSON数据反序列化与序列化的完整代码示例与工程实践指南。

    Java递归出现StackOverflowError的原因分析及栈空间优化方案
    Java递归出现StackOverflowError的原因分析及栈空间优化方案

    详细分析Java递归调用出现StackOverflowError的原因,结合代码说明线程栈、栈帧和递归深度之间的关系,并介绍终止条件排查、迭代改写、减少栈帧开销以及通过-Xss调整JVM栈空间的方法。

    Java空指针异常(NPE)的代码排查与修复实例
    Java空指针异常(NPE)的代码排查与修复实例

    针对Java中常见的NullPointerException(NPE),本文从堆栈日志定位、典型代码触发场景、具体修复代码对比到防御体系进行系统讲解,提供实用且易上手的排查与修复方案。

    selenium上传文件教程本地文件路径设置方法
    selenium上传文件教程本地文件路径设置方法

    深入解析Selenium处理文件上传的技术细节,涵盖标准HTML控件的路径直接注入法与非标准控件的系统级模拟方案,提供Windows与Linux环境下的路径设置最佳实践及常见异常排查。

    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景
    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景

    针对大数据工程场景,系统对比Java、Scala与Python在执行性能、生态结合、开发效率和维护成本上的核心差异,给出清晰的架构选型依据与落地方案。

    查看更多
    精品专题 更多
    装机必备
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows
    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程
    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    Mac软件 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    Aperty macOS版
    Aperty macOS版
    macOS

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    WINDOWS 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    PixTV 网页版
    PixTV 网页版
    Windows

    PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。