区块链技术博客
www.b2bchain.cn

一文带你玩转单例模式

这篇文章主要介绍了一文带你玩转单例模式的讲解,通过具体代码实例进行16469 讲解,并且分析了一文带你玩转单例模式的详细步骤与相关技巧,需要的朋友可以参考下https://www.b2bchain.cn/?p=16469

本文实例讲述了2、树莓派设置连接WiFi,开启VNC等等的讲解。分享给大家供大家参考文章查询地址https://www.b2bchain.cn/7039.html。具体如下:

单例模式

单例模式的特点

七种实现方法

破坏单例模式与解决方案

总结


单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意:

  • 1、单例类只能有一个实例。
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例。

单例模式的特点

  • 意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

  • 主要解决: 一个全局使用的类频繁地创建与销毁。

  • 何时使用: 当您想控制实例数目,节省系统资源的时候。

  • 如何解决: 判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

  • 关键代码: 构造函数是私有的。

  • 使用场景:

    • 1、要求生产唯一***。
    • 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
    • 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

七种实现方法

1、普通的饿汉式

饿汉式使用方法,顾名思义,在一开始就饿,所以直接在类加载时 new 一个对象,并且对外提供一个获取改对象的方法,同时也要私有化构造器,防止外部去 new 这个对象,

特点:这种形式适合多线程,但是在一开始就加载会比较消耗内存。

public class Hungry {     private static Hungry HUNGRY = new Hungry();      private Hungry(){}     //适合多线程     public static Hungry getInstance(){         return HUNGRY;     } } 
2、普通的懒汉式

懒汉式使用方法:顾名思义,就是懒,就是需要的时候再 new,首先要私有化构造器,然后提供一个对外获取对象的方法,在方法中需要判空,当对象为空时去初始化它,然后返回。

特点:适合单线程,虽然在类加载的时候节约了资源,但是在第一次使用时需要初始化,会稍微慢一点。同时如果是多线程的反式,那么还可能会 new 出多个对象,所以是线程不安全的。

public class Lazy {     private static Lazy LAZY;      private Lazy(){}     //适合单线程     private static Lazy getInstance(){         if (LAZY == null){             LAZY = new Lazy();         }         return LAZY;     } } 
3、加锁的懒汉式

使用方法:在普通的懒汉式的基础上再加上一把锁,锁住该对象。

特点:这种写法能够在多线程中很好的工作,但是每次调用 getInstance 方法时都需要进行同步,造成不必要的同步开销,而且大部分时候我们是用不到同步的,所以不建议用这种模式。

public class LazyDouble {     private static LazyDouble LAZY;      private LazyDouble(){}      private synchronized static LazyDouble getInstance(){         if (LAZY == null){             LAZY = new LazyDouble();         }         return LAZY;     } } 
4、双重检查模式(DCL)

使用方法:这种方式需要双重的检查,首先判断是否为空,当为空时进入并且加锁,防止后续线程向下执行,然后需要再判空,防止一起进入 DCL_SINGLE == null 判断的线程再次 new 对象。同时 DCL_SINGLE 对象需要加上 volatile 关键字,防止在多个线程获取对象时, DCL_SINGLE 由于初始化时指令重排,返回了不完整的对象。

特点:资源利用率高,第一次执行getInstance时单例对象才被实例化,效率高。缺点是第一次加载时反应稍慢一些,在高并发环境下也有一定的缺陷,虽然发生的概率很小。DCL虽然在一定程度解决了资源的消耗和多余的同步,线程安全等问题,但是他还是在某些情况会出现失效的问题,也就是DCL失效。所以在《java并发编程实践》一书建议用静态内部类单例模式来替代DCL。

public class DCLSingle {      private volatile static DCLSingle DCL_SINGLE;      private DCLSingle(){}     //双重检测锁模式 懒汉式单例 DCL懒汉式     /**      * 1、分配内存空间      * 2、执行构造方法      * 3、把这个对象指向这个空间      *      * ------> 指令重排会导致问题      */     private static DCLSingle getInstance(){         if (DCL_SINGLE == null){             synchronized (DCLSingle.class){                 if (DCL_SINGLE == null){                     DCL_SINGLE = new DCLSingle();                 }             }         }         return DCL_SINGLE;     } } 
5、静态内部类单例模式

使用:私有化构造器,然后对外提供一个获取的方法,同时定义一个私有的内部类,然后里面 new 一个对象,当程序调用到 getInstance 时,虚拟机才会去加载内部类,并初始化 HOLDER 对象。

特点:第一次加载Singleton类时并不会初始化HOLDER,只有第一次调用getInstance方法时虚拟机加载 InnerClass 并初始化HOLDER ,这样不仅能确保线程安全也能保证Singleton类的唯一性,所以推荐使用静态内部类单例模式。

public class Holder {      private Holder(){}      public static Holder getInstance(){         return InnerClass.HOLDER;     }      public static class InnerClass{         private static final Holder HOLDER = new Holder();     } } 
6、枚举单例

使用:这里不提,枚举类底层也是继承了 Enum 类,由虚拟机去代理

特点:枚举类是线程安全的,这里的安全是任何情况下都安全,不会被反射破坏,后面我们会提到怎么去破坏。

public enum EnumSingle {     INSTANCE; } 
7、使用容器实现单例模式

用 SingletonManager 将多种的单例类统一管理,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。

public class SingletonManager {    private static Map<String, Object> objMap = new HashMap<String,Object>();   private SingletonManager() {    }   public static void registerService(String key, Object instance) {     if (!objMap.containsKey(key) ) {       objMap.put(key, instance) ;     }   }   public static ObjectgetService(String key) {     return objMap.get(key) ;   } } 

破坏单例模式与解决方案

我们使用经典的 DCL 懒汉式来模拟单例模式,然后我们用反射来破坏它

public class DestoryLazy {     private volatile static DestoryLazy LAZY;     private DestoryLazy(){}     private static DestoryLazy getInstance(){         if (LAZY == null){             synchronized (LazyDouble.class){                 if (LAZY == null){                     LAZY = new DestoryLazy();                 }             }         }         return LAZY;     } } 

我们先写一下一段代码,并且我们来看一下它的输出结果

        DestoryLazy instance = DestoryLazy.getInstance();         Constructor<DestoryLazy> declaredConstructor = DestoryLazy.class.getDeclaredConstructor(null);         declaredConstructor.setAccessible(true);         DestoryLazy instance1 = declaredConstructor.newInstance();         System.out.println(instance == instance1);  //false 

我们可以看到 instance 和 instance1 两个 DestoryLazy 对象并不相等。那我们对于这种情况要怎么解决呢?

首先,我们首先想到的是在构造器里面动手脚,当对象已经存在时我们就抛出异常来防止对象被反射破坏。于是我们改一下我们的构造器

public class DestoryLazy {     private volatile static DestoryLazy LAZY;     //修改了构造器     private DestoryLazy(){         synchronized (DestoryLazy.class){             if (LAZY != null){                 throw new RuntimeException("不要试图使用反射来创建对象");             }         }     }    private static DestoryLazy getInstance(){         if (LAZY == null){             synchronized (LazyDouble.class){                 if (LAZY == null){                     LAZY = new DestoryLazy();                 }             }         }         return LAZY;     } } 	//破坏代码     DestoryLazy instance = DestoryLazy.getInstance();     Constructor<DestoryLazy> declaredConstructor = DestoryLazy.class.getDeclaredConstructor(null);     declaredConstructor.setAccessible(true);     DestoryLazy instance1 = declaredConstructor.newInstance();     System.out.println(instance == instance1); /** 抛出异常 Exception in thread "main" 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.base.november.zero_serven.destroysingle.DestoryLazy.main(DestoryLazy.java:59) Caused by: java.lang.RuntimeException: 不要试图使用反射来创建对象 	at com.base.november.zero_serven.destroysingle.DestoryLazy.<init>(DestoryLazy.java:22) 	... 5 more  */ 

但是我们这样做就会安全吗?当我们对象还没有初始化时呢?我们只能杜绝对象初始化后的情况,而无法杜绝初始化前的问题。我们看一下下面的代码。

//转换下顺序 Constructor<DestoryLazy> declaredConstructor = DestoryLazy.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true); DestoryLazy instance1 = declaredConstructor.newInstance(); DestoryLazy instance = DestoryLazy.getInstance(); System.out.println(instance == instance1); //false 

那我们到底要怎样才能保证这个程序里面只有一个对象呢?其实我们可以设置红绿灯(信号量)的形式来解决,我们看一下下面的代码

public class DestoryLazy {     private volatile static DestoryLazy LAZY;     /**      * 红绿灯的做法,设置一个信号量来标志,无法多次获取,但是也有问题      * 如果还没new那么 LAZY 会无法 new 了      */     private static boolean FLAG = false;//信号量     private DestoryLazy(){         synchronized (DestoryLazy.class){             if (FLAG == false){                 FLAG = true;             }else{                 throw new RuntimeException("不要试图使用反射来创建对象");             }         }     }     private static DestoryLazy getInstance(){         if (LAZY == null){             synchronized (LazyDouble.class){                 if (LAZY == null){                     LAZY = new DestoryLazy();                 }             }         }         return LAZY;     } } 

这样就能保证单例了吗?其实这个也还会有两个问题,没错不止一个问题,是两个问题(代码不给了)

  • 其一:我们先来假设一下,如果我们直接利用反射来创建对象那会怎么样呢?也就是我们真正的 LAZY 对象无法创建了。
  • 其二:我们也可以利用反射来修改这个属性,将其值修改成 false,就可以创建多个对象了。

说了这么多问题,那我们到底要怎么保证单例模式呢?我推荐的是使用枚举。使用枚举就能保证对象的单例。

我们来试一下破坏枚举单例吧,我们先看一下它编译后的class文件:

package com.base.november.zero_serven.destroysingle;  public enum EnumDestory {     INSTANCE;      private EnumDestory() {     } }  

然后,我们去使用它的构造方法去创建对象:

EnumDestory instance1 = EnumDestory.INSTANCE; Constructor<EnumDestory> declaredConstructor = EnumDestory.class.getDeclaredConstructor(); declaredConstructor.setAccessible(true); EnumDestory instance2 = declaredConstructor.newInstance(); System.out.println(instance1 == instance2); /** Exception in thread "main" java.lang.NoSuchMethodException: com.base.november.zero_serven.destroysingle.EnumDestory.<init>() 	at java.lang.Class.getConstructor0(Class.java:3082) 	at java.lang.Class.getDeclaredConstructor(Class.java:2178) 	at com.base.november.zero_serven.destroysingle.Test.main(EnumDestory.java:19) */ 

我们看到报的错是没有这个构造方法,我们会不会感到奇怪吗?明明class中有这个构造方法的,接下来我们用反编译命令来看一下class文件。

一文带你玩转单例模式

反编译后还是不行,那接下来我们使用更专业的软件(jad)来反编译它

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.  // Jad home page: http://www.kpdus.com/jad.html  // Decompiler options: packimports(3)  // Source File Name: EnumSingle.java  package com.base.november.zero_serven.destroysingle;  public final class EnumDestory extends Enum {     public static EnumDestory[] values() {         return (EnumDestory[]) $VALUES.clone();     }      public static EnumDestory valueOf(String name) {         return (EnumDestory) Enum.valueOf(com / base / november / zero_serven / destroysingle / EnumDestory, name);     }      private EnumDestory(String s, int i) {         super(s, i);     }      public EnumDestory getInstance() {         return INSTANCE;     }      public static final EnumDestory INSTANCE;     private static final EnumDestory $VALUES[];      static {         INSTANCE = new EnumDestory("INSTANCE", 0);         $VALUES = (new EnumDestory[]{INSTANCE});     } } 

我们可以看到反编译后,居然是这样的,其实枚举类就是继承了 Enum 类,然后会自动赋予每个对象它自己的两个属性,一个是序号一个是名字,这里不做讲解。我们继续来破坏它。

        EnumDestory instance1 = EnumDestory.INSTANCE;         Constructor<EnumDestory> declaredConstructor = EnumDestory.class.getDeclaredConstructor(String.class,int.class);         declaredConstructor.setAccessible(true);         EnumDestory instance2 = declaredConstructor.newInstance();         System.out.println(instance1 == instance2); /** Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects 	at java.lang.reflect.Constructor.newInstance(Constructor.java:417) 	at com.base.november.zero_serven.destroysingle.Test.main(EnumDestory.java:21) */ 

我们可以看到外面是无法创建它的,抛出异常的源码是:

    public T newInstance(Object ... initargs)         throws InstantiationException, IllegalAccessException,                IllegalArgumentException, InvocationTargetException     {         if (!override) {             if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {                 Class<?> caller = Reflection.getCallerClass();                 checkAccess(caller, clazz, null, modifiers);             }         }         if ((clazz.getModifiers() & Modifier.ENUM) != 0)             throw new IllegalArgumentException("Cannot reflectively create enum objects");         ConstructorAccessor ca = constructorAccessor;   // read volatile         if (ca == null) {             ca = acquireConstructorAccessor();         }         @SuppressWarnings("unchecked")         T inst = (T) ca.newInstance(initargs);         return inst;     } 

总结:

  • 掌握单例模式的优缺点
  • 会使用单例模式(七种方法的使用、优缺点)
  • 了解破坏单例模式以及如何解决

本文转自互联网,侵权联系删除一文带你玩转单例模式

赞(0) 打赏
部分文章转自网络,侵权联系删除b2bchain区块链学习技术社区 » 一文带你玩转单例模式
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

b2b链

联系我们联系我们