• Java常用设计模式详解1--单例模式


    单例模式:指一个类有且仅有一个实例

    由于单例模式只允许有一个实例,所以单例类就不可通过new来创建,而所有对象都默认有一个无参的构造函数可以创建对象,所以单例类不仅不能提供public的构造方法,
    还需要重写默认的无参构造方法。由于单例类不可再new创建,所以需要有一个公用的实例需要创建好并返回,所以单例类还需要有一个返回单例对象的方法。且这个方法还必须是静态的方法,否则此方法无法在其他地方调用。综上所述,单例类的大致结构如下:

     1 public class SingletonDemo {
     2 
     3 private static SingletonDemo singleton = new SingletonDemo();//单例的对象实例
     4 
     5 //重写无参构造方法,改为私有private修饰
     6 private SingletonDemo(){
     7 }
     8 
     9 //返回单例对象
    10 public static SingletonDemo getInstance() {
    11 return singleton;
    12 }
    13 }

    或者改为

     1 public class SingletonDemo {
     2 
     3 private static SingletonDemo singleton;//单例的对象实例
     4 
     5 static{
     6 singleton = new singletonDemo();
     7 }
     8 
     9 //重写无参构造方法,改为私有private修饰
    10 private SingletonDemo(){
    11 }
    12 
    13 //返回单例对象
    14 public static SingletonDemo getInstance() {
    15 return singleton;
    16 }
    17 }

    两个方式效果一样,都是在SingletonDemo类加载的时候进行实例化,这种单例方式叫做饿汉式,即类加载的时候就进行了初始化。此时如果这个类的初始化过程比较消耗资源,而这个单例类又一直用不到的话,那么就会浪费过多的资源,如果不想这样的话,还有一种方式是懒汉式,即类加载的时候不进行初始化,而是在使用的时候才初始化。
    大致结构如下:

     1 public class SingletonDemo {
     2 
     3 private static SingletonDemo singleton;// 单例的对象实例
     4 
     5 // 重写无参构造方法,改为私有private修饰
     6 private SingletonDemo() {
     7 }
     8 
     9 // 返回单例对象
    10 public static SingletonDemo getInstance() {
    11 //判断singleton是否初始化,没有则初始化
    12 if (singleton == null) {
    13 singleton = new SingletonDemo();
    14 }
    15 return singleton;
    16 }
    17 }

    这种方式是类加载的时候不进行初始化,而是在使用的时候先判断单例对象是否初始化,没有的话才进行初始化。这种方式虽然解决了饿汉式的消耗资源问题,但是这种方式很显然会有多线程不安全问题,如果两个线程同时执行getInstance方法,而此时singleton都为null,则两个线程都会执行singleton=new singleton(),从而创建了两个实例,很显然违背了单例模式只有一个实例的原则,当然这种方式在单线程的情况下是没有任何问题的。
    但是在多线程情况下就需要让这个getInstance方法变的线程安全,可以加上synchronized关键字进行修饰,如下:

     1 public class SingletonDemo {
     2 
     3 private static SingletonDemo singleton;// 单例的对象实例
     4 
     5 // 重写无参构造方法,改为私有private修饰
     6 private SingletonDemo() {
     7 }
     8 
     9 // 返回单例对象(加锁处理防止多线程并发问题)
    10 public static synchronized SingletonDemo getInstance() {
    11 if (singleton == null) {
    12 singleton = new SingletonDemo();
    13 }
    14 return singleton;
    15 }
    16 }

    此中方式看似没有什么问题,但是单例模式的初始化毕竟只需要初始化一次,为了唯一一次的初始化的时候线程安全而加锁处理,会导致之后每次获取单例实例的时候都会遇到加锁处理,这显然是很影响效率的。所以需要有一种既能延迟加载又是线程安全的方法又不能加锁的方式,使用静态内部类方式便可以解决这一问题,如下:

     1 public class SingletonDemo {
     2 
     3 //静态内部类,包含单例的实例
     4 public static class SingletonDemoHolder{
     5 private static final SingletonDemo singleton = new SingletonDemo();
     6 }
     7 
     8 // 重写无参构造方法,改为私有private修饰
     9 private SingletonDemo() {
    10 }
    11 
    12 // 返回单例对象
    13 public static SingletonDemo getInstance() {
    14 return SingletonDemoHolder.singleton;
    15 }
    16 
    17 }

    这种方式在类加载的时候只会加载SingletonDemo类,而没有加载SingletonDemoHolder类,只有调用了getInstance方法的时候才会加载SingletonDemoHolder,并且才会初始化singleton这个单例对象,这样就达到了延迟加载的效果,而singletonDemoHolder类的加载过程只可能会有一个线程会执行,所以同时也保证了singleton实例不会有多线程安全问题,这也是目前比较普遍的用法。

    But,虽然这种写法能支持懒加载,又解决线程安全性问题,但是还无法保证实例的单一问题,因为Java中创建一个对象不仅仅可以通过new来创建,还可以根据反射和反序列化来创建。这就导致来通过反射和反序列化创建的对象和单例中的对象不是同一个,从而就破坏来单例模式只有一个实例的规则。案例如下:

     1 public class SingletonMain {
     2 
     3 public static void main(String[] args)throws Exception{
     4 Class cla = SingletonDemo.class;//获取Class对象
     5 Constructor constructor = cla.getDeclaredConstructor();//获取构造方法
     6 constructor.setAccessible(true);//设置跳过检查,也就是不检查构造器是否是private修饰
     7 SingletonDemo instance1 = (SingletonDemo)constructor.newInstance();//通过构造器创建对象1
     8 SingletonDemo instance2 = SingletonDemo.getInstance();//通过单例获取对象2
     9 
    10 System.out.println(instance1.toString());
    11 System.out.println(instance2.toString());
    12 }
    13 }

    结果如下:

    1 com.lucky.design.singleton.SingletonDemo@511d50c0
    2 com.lucky.design.singleton.SingletonDemo@60e53b93

    很明显创建了两个不同的SingletonDemo对象,破坏了单例模式

    同样的先通过将单例的实例进行序列化然后再进行反序列化获取到的对象同样也和单例的对象不一样,有兴趣的同学可以自行测试下。而解决方案是在单例类中重写readResolve方法。如下:
    private Object readResolve(){
    return instance;//直接返回单例中的对象
    }
    但是这个解决方法虽然能够防止JDK自动的序列化和反序列化机制,但是无法防止其他的序列化方式,比如alibaba的fastjson的序列化,如下:

    1 public static void main(String[] args)throws Exception{
    2 SingletonDemo instance1 = SingletonDemo.getInstance();
    3 String str = JSON.toJSONString(instance1);
    4 SingletonDemo instance2 = JSON.parseObject(str,SingletonDemo.class);
    5 
    6 System.out.println(instance1.toString());
    7 System.out.println(instance2.toString());
    8 }

    结果为:

    1 com.lucky.design.singleton.SingletonDemo@573fd745
    2 com.lucky.design.singleton.SingletonDemo@78e03bb5


    虽然已经加了readResolve方法,但是还是无法防止所有序列化和反序列化,因为每种序列化和反序列化的算法都是不一样的。

    所以在不考虑反射和序列化的情况下,采用内部类的单例方式就足够了,但是如果考虑这两种情况,显然内部类的方式也不保险。目前而言最保险且最简洁的方式是枚举类的方式,代码如下:

    1 package com.lucky.design.singleton;
    2 /**
    3 * 枚举类单例
    4 * */
    5 public enum SingletonDemo {
    6 intance;
    7 
    8 //其他静态方法
    9 }

    只需要在枚举类中定义一个选项即可,这个唯一选项也就是这个枚举类的唯一实例。测试代码如下:

    1 public static void main(String[] args)throws Exception{
    2 SingletonDemo instance1 = SingletonDemo.intance;
    3 String str = JSON.toJSONString(instance1);
    4 SingletonDemo instance2 = JSON.parseObject(str,SingletonDemo.class);
    5 
    6 System.out.println(instance1.toString());
    7 System.out.println(instance2.toString());
    8 System.out.println(instance1==instance2);
    9 }

    结果为:

    1 intance
    2 intance
    3 true

    很显然达到了单例唯一的效果,而且枚举类的方式的写法还是最简洁的,目前也是最受欢迎的一种写法了。
    枚举类被编译之后默认是继承之抽象了Enum类,且是final类型的,那么枚举类能否避免被反射或是反序列化呢?答案是yes
    首先看下如何避免反射:
    反射的机制是通过获取类的Class对象,如何获取构造器Constructor对象,如何调用newInstance方法来进行创建,那么看下newInstance方法的源码:

     1 @CallerSensitive
     2 public T newInstance(Object ... initargs)
     3 throws InstantiationException, IllegalAccessException,
     4 IllegalArgumentException, InvocationTargetException
     5 {
     6 if (!override) {
     7 if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
     8 Class<?> caller = Reflection.getCallerClass();
     9 checkAccess(caller, clazz, null, modifiers);
    10 }
    11 }
    12 if ((clazz.getModifiers() & Modifier.ENUM) != 0)
    13 throw new IllegalArgumentException("Cannot reflectively create enum objects");
    14 ConstructorAccessor ca = constructorAccessor; // read volatile
    15 if (ca == null) {
    16 ca = acquireConstructorAccessor();
    17 }
    18 @SuppressWarnings("unchecked")
    19 T inst = (T) ca.newInstance(initargs);
    20 return inst;
    21 }


    可以看到当类是Enum类型是,会直接抛出不能反射创建enum类型的对象异常,所以通过反射是无法创建枚举类型的实例

    再看下如何避免被序列化:
    对于序列化和反序列化,因为每一个枚举类型和枚举变量在JVM中都是唯一的,即Java在序列化和反序列化枚举时做了特殊的规定,枚举的writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法是被编译器禁用的,因此也不存在实现序列化接口后调用readObject会破坏单例的问题。

  • 相关阅读:
    7 文件操作
    初识字典1
    软件工程学习进度
    AAAA
    软件工程期末总结
    【操作系统】实验四 主存空间的分配和回收 截止提交时间:2016.6.17
    约教网站开发(一)
    操作系统实验三
    .Scrum团队成立
    数学精灵改进
  • 原文地址:https://www.cnblogs.com/jackion5/p/10914469.html
Copyright © 2020-2023  润新知