温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

java设计模式之怎么实现单例模式

发布时间:2022-11-08 09:51:54 阅读:132 作者:iii 栏目:编程语言
Java开发者专用服务器限时活动,0元免费领,库存有限,领完即止! 点击查看>>

这篇文章主要介绍了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主要应用于:1. web开发;2. Android开发;3. 客户端开发;4. 网页开发;5. 企业级应用开发;6. Java大数据开发;7.游戏开发等。

关于“java设计模式之怎么实现单例模式”这篇文章的内容就介绍到这里,感谢各位的阅读!相信大家对“java设计模式之怎么实现单例模式”知识都有一定的了解,大家如果还想学习更多知识,欢迎关注亿速云行业资讯频道。

亿速云「云服务器」,即开即用、新一代英特尔至强铂金CPU、三副本存储NVMe SSD云盘,价格低至29元/月。点击查看>>

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI

开发者交流群×