当前位置: 首页 > 后端技术 > Java

设计模式第一篇理解单例模式的实现

时间:2023-04-02 09:43:23 Java

设计模式第一篇理解单例模式的实现模式很简单,但是也是设计模式入门的基础,所以我会详细解释一下。DEMO仓库:https://github.com/JanYork/DesignPattern,欢迎PR,共同搭建。单例模式SingletonPattern是Java中最简单的设计模式之一。单例模式一共有-->懒人式、饿鬼式、懒人+同步锁、双重验证锁、静态内部类、枚举。这种类型的设计模式是一种创建模式,它提供了一种创建对象的最佳方式。此模式涉及一个类,该类负责创建自己的对象,同时确保只创建一个对象。此类提供了一种直接访问其唯一对象的方法,而无需实例化此类的对象。单例类必须只有一个实例。单例类必须创建自己的唯一实例。单例类必须将此实例提供给所有其他对象。为什么要使用单例模式,只允许创建一个对象,这样节省内存,加快对象访问速度,所以适合需要共享对象的场合,比如多个模块使用同一个数据源连接对象等。解决全局使用的类频繁创建和销毁的问题。其他场景可以自己想,单例就是全局唯一对象。比如我们熟悉的SpringBean,默认是单例的,是全局唯一的。单例原理单例的原理很简单。唯一能让它不可用的方法就是让它对new不可用,那么我们只需要将类的构造私有化即可:privateClassName(){}但是私有化之后,我们就不能new了,而且如何创建对象呢?我们首先要明白private是私有的,就是不允许其他外部类访问,所以我们还是可以访问的,所以在上面的需求中提到:单例类必须创建自己唯一的实例。同时,我们还需要抛出单例的获取方法。单例模式的惰性风格创建单例类publicclassSlackerStyle{}创建一个属性保存自己的对象publicclassSlackerStyle{privatestaticSlackerStyleinstance;}/***私有构造方法(防止外部new新对象)*/privateSlackerStyle(){}}自己创建对象并获取对象方法publicclassSlackerStyle{privatestaticSlackerStyleinstance;/***私有构造函数(防止外部new新对象)*/privateSlackerStyle(){}/***提供一个静态的public方法,只有在使用该方法时才会创建一个实例*即惰性风格**@returninstance(单例对象)*/publicstaticSlackerStylegetInstance(){if(instance==null){instance=newSlackerStyle();}返回实例;}}当我们调用静态方法时,它会判断上面的静态属性实例中是否有自己的对象,没有-->创建对象并赋值给实例,有-->返回实例。优缺点分析优点:延迟加载,效率高。缺点:线程不安全,可能造成多实例。说明:懒加载-->懒加载方式只会在需要的时候才创建单例对象,可以节省资源,提高程序的启动速度。惰性风格+单例模式下的锁在上面的类中,在getInstance()方法中加入同步锁可以弥补线程不安全的缺陷。/***注意这一段是补充。为了解决线程不安全的问题,可以在方法中加入synchronized关键字,但是这样会导致效率下降。*提供一个静态public方法,添加同步处理代码,解决线程安全问题*该方法是线程安全的slacker风格,即slacker+同步锁,不需要额外写类**@return实例(单例对象)*/publicstaticsynchronizedSlackerStylegetInstance2(){if(instance==null){instance=newSlackerStyle();}返回实例;}虽然弥补了线程不安全的缺陷,但是也损失了一部分效率,所以需要根据业务环境选择合适的方式。.简单盈利模式的饿了么中国风还是和开始一样,创建一个单例类,私有化构造函数。publicclassHungryManStyle{/***privateconstructor(防止外部new对象)*/privateHungryManStyle(){}}对象静态初始化我们的HungryManStyle是懒加载的,也就是先用,然后再去找它先time调用时会创建对象,而Hungry风格正好相反,会在类初始化时创建。静态初始化?我们的static关键字修饰的方法或属性在类加载开始时已经开辟了内存并创建了相关内容。包括各个类:在static{}中也是一样的。所以我们直接使用静态修饰。publicclassHungryManStyle{/***静态变量(单例对象),对象在类加载时初始化(无线程安全问题)*/privatestaticfinalHungryManStyleINSTANCE=newHungryManStyle();/***私有构造函数(防止外部新对象)*/privateHungryManStyle(){}/***提供一个静态公共方法,直接返回INSTANCE**@returninstance(单例对象)*/publicstaticHungryManStylegetInstance(){返回实例;}}当创建类的静态属性时,我们创建了一个新的self对象。优缺点分析饿了么中国式的优点如下:线程安全:由于单例对象是在类加载的时候创建的,所以在多线程环境下不存在同步问题。没有加锁的性能问题:饿了么中国式没有使用同步锁,所以不存在加锁带来的性能问题。实现简单:饿了么中国式实现起来比较简单,在多线程环境下不需要考虑同步问题。饿汉式的缺点如下:立即加载:由于单例对象是在类加载的时候创建的,可能会影响程序的启动速度。资源浪费:如果单例对象很大,在程序中很少使用,Hungry风格可能会浪费资源。综上所述,饿汉式的优点是线程安全,不存在加锁的性能问题,实现简单。缺点是可能会影响程序的启动速度,浪费资源。在选择单例模式的实现方式时,需要根据实际情况综合考虑各种因素,选择最合适的方式。单例模式的双重检查锁初始化了基础单例类的老规矩。publicclassDoubleLockStyle{/***volatile关键字使得实例变量跨多线程可见并禁止指令重排序优化*volatile是一种轻量级的同步机制,即轻量级锁*/privatestaticvolatileDoubleLockStyleinstance;/***私有构造函数(防止外部新建对象)*/privateDoubleLockStyle(){}}不同的是我使用volatile关键字修饰属性。易挥发的?补充知识!这段代码中使用了volatile关键字来保证实例变量的可见性,避免出现空指针异常等问题。volatile是用来修饰变量的修饰符。当一个变量被声明为volatile时,线程在访问该变量时将被强制从主存中读取该变量的值,而不是从线程的本地缓存中读取。使用volatile关键字可以保证多线程间变量访问的可见性和顺序性。当变量被修改时,线程也会将修改后的值强制送回主存,而不是仅仅更新线程的本地缓存。补充:volatile的主要作用是保证共享变量的可见性和顺序。共享变量是指多个线程之间共享的变量,比如单例模式下的实例变量。如果不使用volatile关键字修饰实例变量,在多线程环境下可能会出现空指针异常等问题。这是因为当一个线程修改实例变量的值时,其他线程可能无法立即看到修改后的值,从而导致空指针异常等问题。使用volatile关键字解决了这个问题,因为它保证对共享变量的修改对其他线程可见。除了可见性和排序之外,volatile还可以防止指令重新排序。指令重排序是指CPU为了提高程序执行效率而调整指令执行顺序的行为。在单例模式下,如果实例变量没有声明为volatile,在多线程环境下可能会出现重复创建单例对象的问题。这是因为在多线程环境下,有些线程可能会在实例变量初始化之前调用getInstance()方法,导致多次创建单例对象。通过将实例变量声明为volatile,可以确保实例变量在创建单例对象之前已经正确初始化。doublelock/***提供一个静态public方法,加入doublecheck代码,解决线程安全问题,同时解决懒加载问题*即doublechecklock模式**@returninstance(singletonobject)*/publicstaticDoubleLockStylegetInstance(){if(instance==null){//同步代码块,线程安全的实例创建synchronized(DoubleLockStyle.class){//重新判断的原因是可能有多个线程进入第一个同时判断一个if(instance==null){instance=newDoubleLockStyle();}}}returninstance;}在获取方法中,使用synchronized进行同步,使其线程安全。不足分析双锁模式是一种惰性初始化的优化模式,第一次调用创建单例对象,后续访问直接返回该对象。它使用双重检查锁定(doublecheckedlocking)来保证在多线程环境下只有一个线程可以创建单例对象,并且加锁不会影响程序性能。优点:线程安全:使用双锁模式可以保证在多线程环境下只有一个线程可以创建单例对象,锁不会影响程序性能。惰性初始化:在第一次调用时创建单例对象,避免不必要的资源浪费和内存占用。性能优化:通过使用双重检查锁,可以避免不必要的锁竞争,从而提高程序性能。缺点:实现复杂:双锁模式的实现比较复杂,需要考虑线程安全和性能等因素,容易出错。可读性差:由于双锁模式实现复杂,导致代码可读性差,难以理解和维护。调试难:由于双锁模式涉及多线程并发访问,在调试过程中可能会出现一些难以定位和复现的问题。synchronized为什么叫双锁?在双锁模式下,确实只有一个synchronized关键字,但是这个synchronized关键字在代码中使用了两次,所以称为“双锁”。具体来说,双锁模式通常在getInstance方法中使用synchronized关键字来保证线程安全,但这会影响程序的性能,因为每次访问getInstance方法都需要获取锁。为了避免这个问题,双锁模式使用了一种优化技术,即只有在第一次调用getInstance方法时才获取锁并创建单例对象,后续调用将直接返回创建的单例对象。再次获取锁。在具体实现中,双锁模式会在第一次调用getInstance方法时进行两次检查,分别使用外部if语句和内部synchronized关键字。外层的if语句用来判断单例对象是否已经创建。如果已经创建,则直接返回单例对象。否则会进入内层synchronized关键字块,再次检查单例对象是否已经创建。如果还没有创建,则创建一个单例对象并返回,否则直接返回创建的单例对象。这样做的好处是在多线程环境下,只有一个线程可以进入内部synchronized关键字块,从而保证了线程安全,避免了每次访问getInstance方法都要获取锁的性能问题。单例模式的静态内部类已经很熟悉这个设计模式的原理了,所以直接放代码。publicclassStaticInnerClassStyle{/***私有构造函数(防止外部新对象)*/privateStaticInnerClassStyle(){}/***静态内部类*/privatestaticclassSingletonInstance{//静态内部类中的静态变量(单例对象))privatestaticfinalStaticInnerClassStyleINSTANCE=newStaticInnerClassStyle();/***提供一个直接返回SingletonInstance.INSTANCE的静态公共方法**@returninstance(单例对象)*/publicstaticStaticInnerClassStylegetInstance(){returnSingletonInstance.INSTANCE;}}优缺点分析优点:线程安全:静态内部类在第一次使用时会被加载,所以在多线程环境下,也可以保证只有一个线程创建单例对象,避免了线程安全问题。懒加载:静态内部类模式可以实现懒加载,即只有在第一次调用getInstance方法时才会加载内部类并创建单例对象,避免了第一次调用getInstance方法时创建单例对象的开销程序启动。缺点:需要额外的类:静态内部类模式需要定义一个额外的类来实现单例模式,如果项目中有大量的单例对象,会增加代码量。无法传递参数:静态内部类模式不能接受参数,所以创建单例对象时不能传递参数,可能会限制一些场景。总的来说,静态内部类模式是单例模式的一种高性能、线程安全的实现,适用于大多数场景。如果你需要传递参数或者经常创建单例对象,你可能需要考虑其他的实现方式。不是静态修改吗?为什么lazyloading可以是lazyloading,即懒加载-->用的时候创建对象。在静态内部类模式中,单例对象是在静态内部类内部创建的。静态内部类只有在第一次使用的时候才会加载,所以单例对象也是在第一次使用的时候创建的。这样就达到了懒加载的效果,即只在需要的时候创建单例对象,避免了程序启动时创建单例对象的开销。另外,静态内部类中的静态变量和静态方法都是在类加载时初始化的,静态内部类本身非常轻量级,加载和初始化的时间和开销都非常小。因此,静态内部类模式可以实现懒加载,不会造成太大的性能损失。无论如何,它在静态初始化上是出乎意料的,我相信它对你来说也是出乎意料的。单例模式的枚举单例/***@authorJanYork*@date2023/3/117:54*@description设计模式的单例模式(枚举单例)*优点:避免序列化和反序列化可以通过优化攻击破坏单例,避免反射攻击破坏单例(枚举类型的构造器是私有的),线程安全,懒加载,效率高。*缺点:代码复杂度高。*/publicenumEnumerateSingletons{/***枚举单例*/INSTANCE;publicvoidwhateverMethod(){//TODO:做点什么,在这里实现单例对象的功能}}上面代码中,INSTANCE是EnumSingleton类型的一个枚举常量,代表单例对象的一个??实例。由于枚举类型的特性,INSTANCE会被自动初始化为单例对象的一个??实例,保证在整个应用生命周期中只有一个实例。枚举单例的使用方法很简单,只需要通过EnumSingleton.INSTANCE获取单例对象即可。例如:EnumerateSingletonssingleton=EnumerateSingletons.INSTANCE;singleton.doSomething();使用枚举单例的好处是线程安全,序列化安全,反射安全,代码简洁,不易出错。另外,枚举单例还可以通过枚举类型的特性来添加其他的方法和属性,非常灵活。优缺点分析线程安全:枚举类型的实例创建是在类加载的时候完成的,所以不会有多个线程同时访问和创建单例实例的问题,保证了线程安全。序列化安全:枚举类型默认实现了序列化,因此可以保证序列化和反序列化时单例的一致性。反射安全:由于枚举类型的特殊性,反射机制不会创建多个实例,所以可以保证反射安全。简洁明了:枚举单例的代码非常简洁,易于理解和维护。枚举单例的缺点比较少,但也有一些局限性:不支持懒加载:枚举类型的实例创建是在类加载的时候完成的,所以达不到懒加载的效果。不能继承:枚举类型不能被继承,所以单例类的功能不能通过继承来扩展。在某些情况下使用起来并不方便:比如创建单例对象需要传参的场景,使用枚举单例可能不方便。总之,枚举单例是一个非常好的单例实现。它具有线程安全、序列化安全、反射安全等优点。适用于大多数单例场景,但也有一些限制和限制。需要根据具体场景选择合适的单例实现方式。这么多方式应该怎么选择呢?设计模式是通过优化业务中的一些设计而带来的概念设计。我们需要结合业务分析:饿了么中国式:适用于单例对象小,创建成本低,不需要懒加载的场景。惰性风格:双锁:适用于多线程环境和对性能要求较高的场景。静态内部类:适用于多线程环境和对性能要求较高的场景。枚举:适用于创建单例对象成本较高,需要考虑线程安全、序列化安全、反射安全等问题的场景。如果你的单例对象创建成本低,不需要考虑线程安全、序列化安全、反射安全等问题,推荐使用饿了么来实现单例;如果需要考虑线程安全和性能问题,可以选择惰性双锁或者静态内部类实现;如果需要考虑单例对象创建成本高、线程安全、序列化安全、反射安全等问题,建议选择枚举单例实现。当然,在实际开发中,还需要考虑其他因素,比如单例对象的生命周期、多线程访问、性能要求、并发访问压力等,才能综合选择最合适的单例实现。Java程序员身边的单例模式来自一个AI(敏感词):