目录
一、饿汉式
二、懒汉式
三、枚举实现单例
四、如何避免单例模式反射攻击
首页 Java java教程 一起来分析java设计模式之单例

一起来分析java设计模式之单例

Nov 07, 2022 pm 04:56 PM
java 设计模式 kotlin

本篇文章给大家带来了关于java的相关知识, 其中主要介绍了关于设计模式中单例模式的相关内容,单例就一条基本原则是单例对象的类只会被初始化一次,下面一起来看一下,希望对大家有帮助。

一起来分析java设计模式之单例

推荐学习:《java视频教程

单元素的枚举类型经常成为实现 Singleton 的最佳方法 。

什么是单例?就一条基本原则,单例对象的类只会被初始化一次。在 Java 中,我们可以说在 JVM 中只存在该类的唯一一个对象实例。在 Android 中,我们可以说在程序运行期间,该类有且仅有一个对象实例。

单例模式的简单实现步骤:

  • 构造方法私有,保证无法从外部通过 new 的方式创建对象。

  • 对外提供获取该类实例的静态方法。

  • 类的内部创建该类的对象,通过第 2 步的静态方法返回。

按照上述步骤写下你认为比较严谨的单例模式,然后看看你所写下的单例能否满足以下条件:

  • 你的单例按需加载吗?
  • 你的单例线程安全吗?涉及到并发三要素:原子性、可见性、有序性
  • 你的单例暴力反射和序列化安全吗?

一、饿汉式

//JAVA实现public class SingleTon {    //第三步创建唯一实例
    private static SingleTon instance = new SingleTon();    
    //第一步构造方法私有
    private SingleTon() {
    }    
    //第二步暴露静态方法返回唯一实例
    public static SingleTon getInstance() {        return instance;
    } 
}//Kotlin实现object SingleTon
登录后复制

优点:设计简单 ,解决了多线程实例化的问题。

缺点:在虚拟机加载SingleTon类的时候,将会在初始化阶段为类静态变量赋值,也就是在虚拟机加载该类的时候(此时可能并没有调用 getInstance 方法)就已经调用了 new SingleTon(); 创建了该对象的实例,之后不管这个实例对象用不用,都会占据内存空间。

二、懒汉式

//JAVA实现public class SingleTon {    //创建唯一实例
    private static SingleTon instance = null;    
    private SingleTon() {
    }    
    public static SingleTon getInstance() {        //延迟初始化 在第一次调用 getInstance 的时候创建对象
        if (instance == null) {
            instance = new SingleTon();
        }        return instance;
    } 
}//Kotlin实现class SingleTon private constructor() {    companion object {        private var instance: SingleTon? = null
            get() {                if (field == null) {
                    field = SingleTon()
                }                return field
            }        fun get(): SingleTon{            return instance!!
        }
    }
}
登录后复制

优点:设计也是比较简单的,和饿汉式不同,当这个Singleton被加载的时候,被static修饰的静态变量将会被初始化为null,这个时候并不会占用内存,而是当第一次调用getInstance方法的时候才会被初始化实例对象,按需创建。

缺点:在单线程环境下是没有问题的,在多线程环境下,会产生线程安全问题。在有两个线程同时 运行到了 instane == null这个语句,并且都通过了,那他们就会都各自实例化一个对象,这样就又不是单例了。

如何解决懒汉式在多线程环境下的多实例问题?

  • 静态内部类

    //JAVA实现public class SingleTon {    
        private static class InnerSingleton{        private static SingleTon singleTon  = new SingleTon();
        }    public SingleTon getInstance(){        return InnerSingleton.singleTon;
        }    
        private SingleTon() {
        }
    }//kotlin实现class SingleTon private constructor() {
        companion object {        val instance = InnerSingleton.instance
        }    private object InnerSingleton {        val instance = SingleTon()
        }
    }
    登录后复制
  • 直接同步方法

    //JAVA实现public class SingleTon {    //创建唯一实例
        private static SingleTon instance = null;    
        private SingleTon() {
        }    
        public static synchronized SingleTon getInstance() {        if (instance == null) {
                instance = new SingleTon();
            }        return instance;
        } 
    }//Kotlin实现class SingleTon private constructor() {  companion object {      private var instance: SingleTon? = null
              get() {              if (field == null) {
                      field = SingleTon()
                  }              return field
              }      @Synchronized
          fun get(): SingleTon{          return instance!!
          }
      }
    }
    登录后复制

    优点:加锁只有一个线程能实例该对象,解决了线程安全问题。

    缺点:对于静态方法而言,synchronized关键字会锁住整个 Class,每次调用getInstance方法都会线程同步,效率十分低下,而且当创建好实例对象之后,也就不必继续进行同步了。

    备注:此处的synchronized保证了操作的原子性和内存可见性。

  • 同步代码块(双重检锁方式DCL)

    //JAVA实现 public class SingleTon {    //创建唯一实例
        private static volatile SingleTon instance = null;    
        private SingleTon() {
        }    
        public static SingleTon getInstance() {        if (instance == null) {
                synchronized (SingleTon.class) {   
                    if (instance == null) {
                        instance = new SingleTon();
                    }
                }
            }        return instance;
        } 
    }//kotlin实现class SingleTon private constructor() {    companion object {        val instance: SingleTon by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
                SingleTon() 
            }
      }
    }
    或者class SingleTon private constructor() {    companion object {        @Volatile private var instance: SingleTon? = null
            fun getInstance() =
                  instance ?: synchronized(this) {
                      instance ?: SingleTon().also { instance = it }
                  }
      }
    }
    登录后复制

    优点:添加了一个同步代码块,在同步代码块中去判断实例对象是否存在,如果不存在则去创建,这个时候其实就完全可以解决问题了,因为虽然是多个线程去获取实例对象,但是在同一个时间也只会有一个线程会进入到同步代码块,那么这个时候创建好对象之后,其他线程即便再次进入同步代码块,由于已经创建好了实例对象,便直接返回即可。但是为什么还要在同步代码块的上一步再次去判断instance为空呢?这个是由于当我们创建好实例对象之后,直接去判断此实例对象是否为空,如果不为空,则直接返回就好了,就避免再次进去同步代码块了,提高了性能。

    缺点:无法避免暴力反射创建对象。

    备注:此处的volatile发挥了内存可见性及防止指令重排序作用。

三、枚举实现单例

public enum SingletonEnum {    INSTANCE;    public static void main(String[] args) {        System.out.println(SingletonEnum.INSTANCE == SingletonEnum.INSTANCE);
    }
}
登录后复制

枚举实现单例是最为推荐的一种方法,因为就算通过序列化,反射等也没办法破坏单例性。(关于Android使用枚举会产生性能问题的说法,这应该是Android 2.x系统之前内存紧张的时代了,现在已经Android 13了,相信某些场合枚举所带来的便利远远大于这点所谓的性能影响)

四、如何避免单例模式反射攻击

以最初的DCL为测试案例,看看如何进行反射攻击及又如何在一定程度上避免反射攻击。反射攻击代码如下:

 public static void main(String[] args) {

     SingleTon singleton1 = SingleTon.getInstance();
     SingleTon singleton2 = null;

     try {
         Class<SingleTon> clazz = SingleTon.class;
         Constructor<SingleTon> constructor = clazz.getDeclaredConstructor();
         constructor.setAccessible(true);
         singleton2 = constructor.newInstance();
     } catch (Exception e) {
         e.printStackTrace();
     }

     System.out.println("singleton1.hashCode():" + singleton1.hashCode());
     System.out.println("singleton2.hashCode():" + singleton2.hashCode());
 }
登录后复制

执行结果:

 singleton1.hashCode():1296064247
 singleton2.hashCode():1637070917
登录后复制

通过执行结果发现通过反射破坏了单例。 如何保证反射安全呢?只能以暴制暴,当已经存在实例的时候再去调用构造函数直接抛出异常,对构造函数做如下修改:

  public class SingleTon {     //创建唯一实例
     private static volatile SingleTon instance = null;   
     private SingleTon() {         if (instance != null) {             throw new RuntimeException("单例构造器禁止反射调用");
         }
     }   
     public static SingleTon getInstance() {         if (instance == null) {
           synchronized (SingleTon.class) {   
               if (instance == null) {
                   instance = new SingleTon();
               }
           }
       }       return instance;
     } 
 }
登录后复制

此时可防御反射攻击,抛出异常如下:

 java.lang.reflect.InvocationTargetException
 at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
 at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
 at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
 at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
 at com.imock.demo.TestUtil.testSingleInstance(TestUtil.java:45)
 at com.imock.demo.TestUtil.main(TestUtil.java:33)
 Caused by: java.lang.RuntimeException: 单例构造器禁止反射调用
 at com.imock.demo.SingleTon.<init>(SingleTon.java:16)
 ... 6 more Exception in thread "main" java.lang.NullPointerException
 at com.imock.demo.TestUtil.testSingleInstance(TestUtil.java:49)
 at com.imock.demo.TestUtil.main(TestUtil.java:33) 
 Process finished with exit code 1
登录后复制

然后我们把上述测试代码修改如下(调换了singleton1的初始化顺序)

 public static void main(String[] args) {
     SingleTon singleton2 = null;

     try {
         Class<SingleTon> clazz = SingleTon.class;
         Constructor<SingleTon> constructor = clazz.getDeclaredConstructor();
         constructor.setAccessible(true);
         singleton2 = constructor.newInstance();
     } catch (Exception e) {
         e.printStackTrace();
     }

     System.out.println("singleton2.hashCode():" + singleton2.hashCode());

     SingleTon singleton1 = SingleTon.getInstance(); //调换了位置,在反射之后执行
     System.out.println("singleton1.hashCode():" + singleton1.hashCode());
 }
登录后复制

执行结果:

 singleton2.hashCode():1296064247
 singleton1.hashCode():1637070917
登录后复制

发现此防御未起到作用。

缺点:

  • 如果反射攻击发生在正常调用getInstance之前,每次反射攻击都可以获取单例类的一个实例,因为即使私有构造器中使用了静态成员(instance) ,但单例对象并没有在类的初始化阶段被实例化,所以防御代码不生效,从而可以通过构造器的反射调用创建单例类的多个实例;
  • 如果反射攻击发生在正常调用之后,防御代码是可以生效的;

如何避免序列化攻击?只需要修改反序列化的逻辑就可以了,即重写 readResolve() 方法,使其返回统一实例。

   protected Object readResolve() {       return getInstance();
   }
登录后复制

脆弱不堪的单例模式经过重重考验,进化成了完全体,延迟加载,线程安全,反射及序列化安全。简易代码如下:

  • 饿汉模式

    public class SingleTon {    private static SingleTon instance = new SingleTon();    
        private SingleTon() {        if (instance != null) {              throw new RuntimeException("单例构造器禁止反射调用");
             }
        }    public static SingleTon getInstance() {        return instance;
        } 
    }
    登录后复制
  • 静态内部类

    public class SingleTon {    
        private static class InnerStaticClass{        private static SingleTon singleTon  = new SingleTon();
        }    public SingleTon getInstance(){        return InnerStaticClass.singleTon;
        }    
        private SingleTon() {       if (InnerStaticClass.singleTon != null) {           throw new RuntimeException("单例构造器禁止反射调用");
           }
        }
    }
    登录后复制
  • 懒汉模式

    public class SingleTon {    //创建唯一实例
        private static SingleTon instance = null;    
        private SingleTon() {        if (instance != null) {              throw new RuntimeException("单例构造器禁止反射调用");
            }
        }    
        public static SingleTon getInstance() {        //延迟初始化 在第一次调用 getInstance 的时候创建对象
            if (instance == null) {
                instance = new SingleTon();
            }        return instance;
        } 
    }
    登录后复制

    缺点:

    • 如果反射攻击发生在正常调用getInstance之前,每次反射攻击都可以获取单例类的一个实例,因为即使私有构造器中使用了静态成员(instance) ,但单例对象并没有在类的初始化阶段被实例化,所以防御代码不生效,从而可以通过构造器的反射调用创建单例类的多个实例;
    • 如果反射攻击发生在正常调用之后,防御代码是可以生效的。

(枚举实现单例是最为推荐的一种方法,因为就算通过序列化,反射等也没办法破坏单例性,底层实现比如newInstance方法内部判断枚举抛异常)

推荐学习:《java视频教程

以上是一起来分析java设计模式之单例的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
4 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
4 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
4 周前 By 尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解锁Myrise中的所有内容
1 个月前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

Java 中的完美数 Java 中的完美数 Aug 30, 2024 pm 04:28 PM

Java 完美数指南。这里我们讨论定义,如何在 Java 中检查完美数?,示例和代码实现。

Java 中的随机数生成器 Java 中的随机数生成器 Aug 30, 2024 pm 04:27 PM

Java 随机数生成器指南。在这里,我们通过示例讨论 Java 中的函数,并通过示例讨论两个不同的生成器。

Java中的Weka Java中的Weka Aug 30, 2024 pm 04:28 PM

Java 版 Weka 指南。这里我们通过示例讨论简介、如何使用weka java、平台类型和优点。

Java 中的史密斯数 Java 中的史密斯数 Aug 30, 2024 pm 04:28 PM

Java 史密斯数指南。这里我们讨论定义,如何在Java中检查史密斯号?带有代码实现的示例。

Java Spring 面试题 Java Spring 面试题 Aug 30, 2024 pm 04:29 PM

在本文中,我们保留了最常被问到的 Java Spring 面试问题及其详细答案。这样你就可以顺利通过面试。

突破或从Java 8流返回? 突破或从Java 8流返回? Feb 07, 2025 pm 12:09 PM

Java 8引入了Stream API,提供了一种强大且表达力丰富的处理数据集合的方式。然而,使用Stream时,一个常见问题是:如何从forEach操作中中断或返回? 传统循环允许提前中断或返回,但Stream的forEach方法并不直接支持这种方式。本文将解释原因,并探讨在Stream处理系统中实现提前终止的替代方法。 延伸阅读: Java Stream API改进 理解Stream forEach forEach方法是一个终端操作,它对Stream中的每个元素执行一个操作。它的设计意图是处

Java 中的时间戳至今 Java 中的时间戳至今 Aug 30, 2024 pm 04:28 PM

Java 中的时间戳到日期指南。这里我们还结合示例讨论了介绍以及如何在java中将时间戳转换为日期。

Java程序查找胶囊的体积 Java程序查找胶囊的体积 Feb 07, 2025 am 11:37 AM

胶囊是一种三维几何图形,由一个圆柱体和两端各一个半球体组成。胶囊的体积可以通过将圆柱体的体积和两端半球体的体积相加来计算。本教程将讨论如何使用不同的方法在Java中计算给定胶囊的体积。 胶囊体积公式 胶囊体积的公式如下: 胶囊体积 = 圆柱体体积 两个半球体体积 其中, r: 半球体的半径。 h: 圆柱体的高度(不包括半球体)。 例子 1 输入 半径 = 5 单位 高度 = 10 单位 输出 体积 = 1570.8 立方单位 解释 使用公式计算体积: 体积 = π × r2 × h (4

See all articles